Page 1 of 2

Live Texture Painting Script

PostPosted: Mon Jan 06, 2014 11:14 pm
by stratified
Hi all,

In release candidate 1.25, we replaced the IMAGE_LABEL by the BITMAP component. The BITMAP component is like a paint canvas that can react to mouse events so you can write some pretty cool scripts with it. The bitmap is low-level (pixel level) so it may be a bit cumbersome. But because it's so low level it would allow you to do some pretty powerful stuff (e.g. creating your own components, draw paths, display the scene, ...).

In this example we do live texture painting. Just start rendering something, select a texture node and start the script. Now you should by able to paint some stuff on your selected texture. You can also use it to paint a texture environment. This is a proof-of-concept, not a production stable script!

First, the selected texture's buffer is loaded in the bitmap. Then we show the bitmap in a window and start listening for mouse events. When the mouse is dragged on the bitmap, we draw a red square in the bitmap and update the texture's buffer with the red square. Important here is the call to octane.changemanager.update(), each time you want to see your changes reflected in the current render, you should call this function. (The idea is that you make all your changes and then call this function once to optimize things).

Here's the code:

Code: Select all
-- Live texture painting script.
-- Proof of concept, don't use this to launch nuclear missiles!
--
-- @version Live Texture Painting
-- @author  Thomas Loockx
-- @version 0.2


-- Retrieves the colour of a texel.
local function getTexPixel(tex, x, y)
    local ix = 4 * ((x-1) + tex.size[1] * (y-1)) + 1
    return { tex.texels[ix], tex.texels[ix+1], tex.texels[ix+2], tex.texels[ix+3] }
end

-- Sets a pixel in our texel data structure.
local function setTexPixel(tex, x, y, cl)
    -- bounds check
    if x > tex.size[1] or x < 1 then return end
    if y > tex.size[1] or y < 1 then return end

    local ix = 4 * ((x-1) + tex.size[1] * (y-1)) + 1
   
    -- bounds check on the index
    if (ix > #tex.texels) then return end

    tex.texels[ix]   = cl[1]
    tex.texels[ix+1] = cl[2]
    tex.texels[ix+2] = cl[3]
    tex.texels[ix+3] = cl[4]
end


-- get all the info about the selected texture
local TEX = {}
TEX.node = octane.project.getSelection()[1]
assert(TEX.node and TEX.node:getProperties().type == octane.NT_TEX_IMAGE, "please select an image texture node")
TEX.size   = TEX.node:getAttribute(octane.A_SIZE)
TEX.texels = TEX.node:getAttribute(octane.A_BUFFER)
print(string.format("texture %d x %d, buffer len %d", TEX.size[1], TEX.size[2], #TEX.texels))

-- clear the filename attributes. This tricks Octane into thinking that the texture
-- came directly from the image buffer.
TEX.node:clearAttribute(octane.A_FILENAME, false)
TEX.node:clearAttribute(octane.A_PACKAGE, false)

function mouseCallback(bitmap, event, x, y, dx, dy)
    if event == octane.gui.eventType.MOUSE_DOWN or event == octane.gui.eventType.MOUSE_DRAG then
    print(x, y)
        -- paint brush settings
        local thickness = 5
        local colour    = { 255, 0, 0, 255 }
       
        for i=x,x+thickness do
            for j=y,y+thickness do
                bitmap:setPixel(i, j, colour)
                setTexPixel(TEX, i, TEX.size[2] - j - 1, colour)
            end
        end

        -- update the texture node
        TEX.node:setAttribute(octane.A_BUFFER, TEX.texels)
        -- update the render engine
        octane.changemanager.update()
    end
end

-- create a plain vanilla bitmap same size as the texture
local bitmap = octane.gui.create
{
    type             = octane.gui.componentType.BITMAP,
    width            = TEX.size[1],
    height           = TEX.size[2],
    opacity          = 0.5,
    backgroundColour = { 255, 255, 255, 0 },
    mouseCallback    = mouseCallback,
}

-- fill the bitmap with the texture colours
for x=1,TEX.size[1] do
    for y=1,TEX.size[2] do
        -- we need to flip the texture to display it properly
        bitmap:setPixel(x, TEX.size[2]-y, getTexPixel(TEX, x, y))
    end
end


local group = octane.gui.create
{
    type     = octane.gui.componentType.GROUP,
    rows     = 1,
    cols     = 1,
    children = { bitmap },
    border   = false,
    inset    = { 5 },
    padding  = { 5 },
}

local window = octane.gui.create
{
    type     = octane.gui.componentType.WINDOW,
    children = { group },
    width    = group:getProperties().width,
    height   = group:getProperties().height,
    text     = "Texture Painter",
}

window:showWindow()


Here's a result of my artistic skills:

paint.jpg
texture painting


texture_paint.png
my skills


Some ideas for improvement:

  • Modify the brush colour, shape and size (1.25 features a colour swatch as well to pick a colour).
  • Allow saving of the texture
  • Don't screw up the original texture
  • Make it work for HDRI images.
  • It's slow for larger textures.
  • ...

cheers,
Thomas

Re: Live Texture Painting Script

PostPosted: Mon Jan 06, 2014 11:20 pm
by grimm
I'm thinking QA production tool? Looks like fun. :)

Jason

Re: Live Texture Painting Script

PostPosted: Mon Jan 06, 2014 11:44 pm
by Tugpsx
Guys I think you found King Solomon's mine when you added Lua scripting to Octane, can't wait to see the goodies that get created with this and maybe some interesting add-ons.
Thanks again.

Re: Live Texture Painting Script

PostPosted: Tue Jan 07, 2014 12:02 am
by stratified
Yeah, we hope to see some killer apps in the future ;)

cheers,
Thomas

Re: Live Texture Painting Script

PostPosted: Tue Jan 07, 2014 12:57 am
by pixelrush
Very nifty. Can you make size pressure sensitive for a Wacom tablet? A simple circular brush with opacity is ok. :) 3d markups would be cool.
Yeah, I know what Uncle Roeland is going to say... :roll:

Re: Live Texture Painting Script

PostPosted: Tue Jan 07, 2014 2:04 am
by UnCommonGrafx
That's awfully cool. Made me pay attention to what the future holds for octane.
Perhaps this is a benefit to being 'late to the party' of renderers: out of the box can actually occur now that the box has been clearly defined.

Re: Live Texture Painting Script

PostPosted: Tue Jan 07, 2014 2:52 am
by stratified
pixelrush wrote:Very nifty. Can you make size pressure sensitive for a Wacom tablet? A simple circular brush with opacity is ok. :) 3d markups would be cool.
Yeah, I know what Uncle Roeland is going to say... :roll:


pressure sensitive is not going to work right now. There's not enough info in the mouse event to do that. You could modify the brush size with the scroll wheel (or use that to zoom on the texture).

Or you can do something more funky like creating some pictures of shapes (triangle, circle, square, ...). Display those in a bitmap on the component and then when clicked change the shape of the brush.

cheers,
Thomas

Re: Live Texture Painting Script

PostPosted: Tue Jan 07, 2014 3:08 am
by pixelrush
Well my tablet has a touch strip I can use for scroll purposes so we aren't defeated yet ;)
Please do that. Brush size <-> scroll wheel.
Funk mode brush swapping sounds way cool. 8-)

Re: Live Texture Painting Script

PostPosted: Tue Jan 07, 2014 4:30 am
by Tugpsx
Interesting, I wonder if this procedure could be applied to a painted path scenario (although the math behind it may get interesting) since moving the brush in 3 space and tracking the paths in an array and using those as waypoint rails for a camera system is turning Octane into way more than we would bargain for.

Just more crazy ideas.

Re: Live Texture Painting Script

PostPosted: Tue Jan 07, 2014 4:39 am
by stratified
Yep, that's possible. It would require a lot of work, all you can do in a bitmap is load pictures, capture mouse events or manipulate pixels. So it's powerful but really low level. It's like a step back to 80's game programming (not that I would now) ...

You could start with drawing in a camera path in 2D. This means that the camera would move in the same plane.

If somebody's keen, I think it's interesting to create a 2D draw library on top of this to draw primitives like circles, arcs, rectangles, ... We might add them in the future but we don't have the bandwidth right now.

cheers,
Thomas