Ignatz’ s pixel-perfect touch detection in Craft

IMG_7197

Ever since @John said it should be possible, I’ve wanted to do it: implement @Ignatz’s approach for pixel-perfect touch detection in Craft.

It works great, with one problem.

I have to render each object in silhouette, to a buffer, but once I make them render as silhouettes, I can’t get them back to their original look.

Can anyone help me get them back to their original look?

Project:
GetEntityByTouch.zip (35.6 KB)

Or if you prefer, code:


-- setup function
function setup()
    --define assets for entities
    assetList = {
        asset.builtin.Blocky_Characters.ManAlternative,
        asset.builtin.Blocky_Characters.Orc,
        asset.builtin.Watercraft.watercraftPack_003_obj,
        asset.builtin.Watercraft.watercraftPack_024_obj,
        asset.builtin.CastleKit.metalGate_obj,
        asset.builtin.CastleKit.siegeCatapult_obj
    }
    
    --setup scene
    makeScene()
    --register custom shader with the scene
    craft.shader.add(customShaderUnlit)
    
    --setup buffer to draw to for touch detection
    makeTouchBuffer()

    -- Create and position entities within the visible area
    zDepth = 50
    local lowerLeft, upperRight = craftCoordsForScreenCornersAtDepth(zDepth)
    makeEntitiesInsideBounds(lowerLeft, upperRight, zDepth)
end

function draw()
    drawThumbnail()
end

-- touch function to handle touch events
function touched(touch)
    touches.touched(touch)
    if touch.state == BEGAN then
        renderToBuffer()
        checkTouch(touch)
    end
end

function drawThumbnail()
    pushStyle()
    spriteMode(CORNER)
    -- Define the size of the thumbnail
    local thumbnailSize = vec2(WIDTH/5, HEIGHT/5)  -- Adjust as needed
    -- Define the position of the thumbnail
    local thumbnailPos = vec2(WIDTH - thumbnailSize.x - 20, 20)    
    -- Draw the thumbnail
    sprite(touchBuffer, thumbnailPos.x, thumbnailPos.y, thumbnailSize.x, thumbnailSize.y)
    popStyle()
end

-- Updated checkTouch function
function checkTouch(touch)
    local r, g, b, a = touchBuffer:get(math.floor(touch.x), math.floor(touch.y))
    for i, entity in ipairs(entities) do
        local entityColor = entity.pickColor
        if entityColor.r == r and entityColor.g == g and entityColor.b == b then
            print("Touched entity: "..entity.number..", "..entity.modelName)
            return
        end
    end
    print("No entity touched")
end

function makeScene()
    craft.scene.main = craft.scene()
    local scene = craft.scene.main
    scene.sky.material.sky = color(72, 235, 156)       
    scene.sky.material.horizon = color(30, 47, 66)       
    scene.sky.material.ground = color(52, 90, 75)   
    scene.camera:add(OrbitViewer,vec3(0,0, 0), 20, 0,1000)
end

function makeTouchBuffer()
    touchBuffer = image(WIDTH, HEIGHT)
    bufferBGColor = color(255, 0, 202)
    uniqueColors[bufferBGColor] = true
end

function craftCoordsForScreenCornersAtDepth(zDepth)
    -- Ensure the scene is updated and drawn at least once before calling screenToWorld
    craft.scene.main:update(DeltaTime)
    craft.scene.main:draw()
    -- Get screen boundaries in Craft coordinates
    local lowerLeft = craft.scene.main.camera:get(craft.camera):screenToWorld(vec3(0, 0, zDepth))
    local upperRight = craft.scene.main.camera:get(craft.camera):screenToWorld(vec3(WIDTH, HEIGHT, zDepth))
    return lowerLeft, upperRight
end

function makeEntitiesInsideBounds(lowerLeft, upperRight, zDepth)
    entities = {}
    local entityCounter = 0
    for i, assetName in ipairs(assetList) do
        for j = 1, 3 do
            local entity = craft.scene.main:entity()
            entity.model = craft.model(assetName)
            entityCounter = entityCounter + 1
            entity.number = entityCounter
            entity.modelName = string.match(tostring(assetName), ".+/(.+)%.") or assetName  -- Extracts the name between the last slash and the last period
            -- Generate random position within the visible area
            local x = math.random() * (upperRight.x - lowerLeft.x) + lowerLeft.x
            local y = math.random() * (upperRight.y - lowerLeft.y) + lowerLeft.y
            
            -- Assign position and random rotation to the entity
            entity.position = vec3(x, y, zDepth)
            entity.rotation = quat.eulerAngles(math.random(0, 360), math.random(0, 360), math.random(0, 360))
            
            -- Give it a random color
            entity.pickColor = getUniqueColor()
            
            -- Create a small colored image for this entity
            entity.colorImage = image(1, 1) -- Create a 1x1 pixel image
            entity.colorImage:set(1, 1, entity.pickColor) -- Set the color of the pixel
            
            -- Store entities for later use
            table.insert(entities, entity)
        end
    end
end

-- Global table to store unique colors
uniqueColors = {}
-- Function to get a unique random color
function getUniqueColor()
    while true do
        local col = color(math.random(255), math.random(255), math.random(255))
        local key = tostring(col)
        if uniqueColors[key] == nil then
            uniqueColors[key] = true
            return col
        end
    end
end

function renderToBuffer()
    setContext(touchBuffer)
    craft.scene.main.sky.active = false
    craft.scene.main.camera:get(craft.camera).clearColor = bufferBGColor
    
    for i, entity in ipairs(entities) do
        entity.originalMaterial = entity.material
        if entity.originalMaterial then
            entity.originalMap = entity.material.map
            entity.originalOffsetRepeat = entity.material.offsetRepeat
        end
        entity.material = craft.material("Custom Unlit") -- Assign the shader by name
        entity.material.map = entity.colorImage
    end
    
    craft.scene.main:update(DeltaTime)
    craft.scene.main:draw()
    
    -- Reset materials and maps to their original state
    for i, entity in ipairs(entities) do
        if entity.originalMaterial then
            entity.material = entity.originalMaterial
            entity.material.map = entity.originalMap
        else
            entity.material = craft.material(asset.builtin.Materials.Standard)
            entity.material.map = readImage(asset.builtin.Blocky_Characters.OrcSkin)
        end 
    end
    craft.scene.main.sky.active = true
    setContext()
end

-- Custom Shader

customShaderUnlit = {
    name = "Custom Unlit",
    
    options =
    {
        USE_COLOR = { true },
    },
    
    properties =
    {
        --unless diffuse is specifically added to a custom shader you won't be able to externally modify it on the material
        map = {"texture2D", nil},
        diffuse = {"vec3", vec3(1, 1, 1)}
    },
    
    pass =
    {
        base = "Surface",
        
        blendMode = "disabled",
        depthWrite = true,
        depthFunc = "lessEqual",
        renderQueue = "solid",
        colorMask = {"rgba"},
        cullFace = "back",
        
        vertex =
        [[
        void vertex(inout Vertex v, out Input o)
        {}
    ]],


surface =
[[
uniform sampler2D map;
uniform vec3 diffuse;

void surface(in Input IN, inout SurfaceOutput o)
{                
    o.diffuse = texture(map, IN.uv * 10.0).rgb * diffuse;
    o.emissive = 1.0;                                
    o.emission = vec3(0.0, 0.0, 0.0);
}
]]
}
}


customShaderPhysical = {
name = "Custom Physical",

options =
{
USE_COLOR = { true },
USE_LIGHTING = { true },
STANDARD = { true },
PHYSICAL = { true },
ENVMAP_TYPE_CUBE = { true },
ENVMAP_MODE_REFLECTION = { true },
USE_ENVMAP = { false, {"envMap"} },
},

properties =
{
envMap = { "cubeTexture", "nil" },
envMapIntensity = { "float", "0.75" },
refactionRatio = { "float", "0.5" },
diffuse = {"vec3", vec3(1, 1, 1)}
},

pass =
{
base = "Surface",

blendMode = "disabled",
depthWrite = true,
depthFunc = "lessEqual",
renderQueue = "solid",
colorMask = {"rgba"},
cullFace = "back",

vertex =
[[
void vertex(inout Vertex v, out Input o)
{
}
]],

surface =
[[
uniform vec3 diffuse;

void surface(in Input IN, inout SurfaceOutput o)
{
o.diffuse = vec3(1.0, 1.0, 1.0) * diffuse;
o.roughness = 0.32468;
o.metalness = 0.0;
o.emission = vec3(0.0, 0.0, 0.0);
o.emissive = 1.0;
o.opacity = 1.0;
o.occlusion = 1.0;
}
]]
}
}

I just popped in to say how impressed I am that you are so persistent. That’s how I had to be to find this solution in the first place.

That’s a real success attitude for life. All the best!

2 Likes

I wish I could take your accolades unadorned by my own procrastination. Codea is one of my favorite things to do when I should be doing something else. :slight_smile:

So, I can think of a kludge for this, but it’s fairly ridiculous.

I could have a duplicate of each object, make it use the single-color material, and during buffer draws draw that object instead of the original.

So I need two copies of each object — seems ridiculous but I don’t know what else to do!

@John, it seems like the coloring information for lots of these models isn’t available through entity.material.mat. Is it possible to get off of the model somehow, and then restore it after the single-color material is applied?