I found craft.voxels to be an excellent system and tried to organize its hierarchy

@dave1707 There should be a block limit, and I suspect there may be a pre determined maximum memory allocation for blocks. In the sample “Block Library” for modeling, a block can not be larger than vec3(255,255,255?voxels.

  • addElement()
    • lower >= vec3(0,0,0)
    • upper <= vec3(255,255,255)

I guess it could be because of that limitation.
Your question prompted me to take a hard look at the use of the resize() method, which I used to fill in the numbers by feeling. I, too, am on the road to learning voxels, Learn a little bit more every day.

@Bri_G You mean LOD? In fact, I specifically kept the drawing size at the edge of the crash because it was more helpful in finding hidden problems. If you want a more stable example, you can scale it down a bit, like the thickness of the ground, the radius of the sphere can be reduced, and so on.

@John @binaryblues - just in case you didn’t believe me - here’s a crash video.

Craft crash

@Bri_G I’m sure you’ve come across crash, which is so easy to trigger in recent beta versions, I recently came across two crash, one with this line:

shader("D:a")

or

shader("Documents:a")

The other is that I recently searched all the voxels related discussions in the forums and found a voxels system written by an early user that would crash in a few seconds.I’m gonna clean it up and put it up. I tried to read the code to locate the problem, but I couldn’t figure out why the crash happened

Here is the early voxels propotype:

This is the link:

https://codea.io/talk/discussion/982/voxel-shader-engine-code-relea

@skar Are those 16K meshes separate Mesh objects?

If so, the number of draw() calls is probably your bottleneck there. I’d imagine craft.voxels lowers the number of OpenGL draw calls drastically to something manageable and more performant.

Fwiw, the overhead of draw calls in OpenGL is one of the main driving forces for the creation of newer graphics APIs like Metal & Vulkan.

Example9: More personalized simple block type

New chages:

  • You can set the texture map and the color on each side, using the code like:
dirt.setTexture(UP, "Blocks:Dirt")
dirt.setTexture(DOWN, "Blocks:Stone")
dirt.setTexture(EAST, "Blocks:Grass2")
dirt.setTexture(NORTH, "Blocks:Grass1")
dirt.setTexture(WEST, "Blocks:Leaves")
dirt.setTexture(SOUTH, "Blocks:Oven")

dirt.setColor(UP, color(255))
...
dirt.setColor(SOUTH, color(255))
  • Added a box with a maximum of 255 * 255 * 255 so you can locate coord

@John I found two problems:

  • NORTH will use the same texture map as SOUTH, It’s not working on its own.

  • The tinted switch works the other way around. When I set it to true, the block will go dark.

The code:

viewer.mode = FULLSCREEN

function setup()
    scene = craft.scene()
    scene.sky.material.sky = color(80, 203, 233)
    scene.sky.material.horizon = color(80, 108, 233)
    
    player = scene.camera:add(OrbitViewer, vec3( 40, 25, 40), 8, 1, 4000)
    
    -- set voxel terrain parameters: size, coord
    scene.voxels:resize(vec3(5,1,5),vec3(16,128,16))      
    scene.voxels.coordinates = vec3(0,0,0)    
    
    -- Create 2 new block types: "simpleGrass", "simpleDirt1"
    createSimpleBlockType("simpleGrass", "Blocks:Grass Top", color(201, 233, 80))
    
    -- Using block type: simpleGrass as the filling unit
    scene.voxels:fill("simpleGrass")    
    -- Create a box using "simpleGrass" block type, every unit in the box is a "simpleGrass" block
    scene.voxels:box(0,0,0, 16*8,10,16*8)
    
    -- Using new block type: TreeGenerator as the filling unit
    scene.voxels:fill("simpleGrass")
    -- A single block
    scene.voxels:block(vec3(30,25,35))
    -- Create a sphere and a line using "simpleGrass" block type, 
    -- every unit in those two shapes is a "simpleGrass" block
    scene.voxels:line(10,20,0,10,30,40)
    scene.voxels:sphere(40,20,60,4)
    -- craft.scene.main = scene
end


function createSimpleBlockType(blockTypeName,texture,blockColor)
    -- Create user-defined block type
    scene.voxels.blocks:addAssetPack("Blocks")
    
    local dirt = scene.voxels.blocks:new(blockTypeName)
    ---[[
    dirt.setTexture(ALL, texture or "Blocks:Dirt")
    dirt.setTexture(UP, "Blocks:Dirt")
    dirt.setTexture(DOWN, "Blocks:Stone")
    dirt.setTexture(EAST, "Blocks:Grass2")
    dirt.setTexture(NORTH, "Blocks:Grass1")
    dirt.setTexture(WEST, "Blocks:Leaves")
    dirt.setTexture(SOUTH, "Blocks:Oven")
    --]]
    
    ---[[
    dirt.setColor(ALL, blockColor or color(251))
    dirt.setColor(UP, color(255))
    dirt.setColor(DOWN, color(238, 235, 235))
    dirt.setColor(EAST, color(5, 235, 239))
    dirt.setColor(NORTH, color(249, 248, 248))
    dirt.setColor(WEST, color(249))
    dirt.setColor(SOUTH, color(255))
    --]]
    
    dirt.tinted = false
end

function update(dt)
    scene:update(dt)
    blockRange()
end

function draw()
    update(DeltaTime)    
    scene:draw()
end

-- Draw a 256*256*256 box, from vec3(0,0,0) to vec3(255,255,255), it is the block type range.
function blockRange()
    scene.debug:line(vec3(0, 0, 0),vec3(255, 0, 0),color(47, 239, 5))
    scene.debug:line(vec3(0,0,0),vec3(0, 255, 0),color(239, 5, 5))
    scene.debug:line(vec3(0,0,0),vec3(0, 0, 255),color(5, 41, 239))
    
    scene.debug:line(vec3(255,255,255),vec3(0, 255, 255),color(239, 91, 5))
    scene.debug:line(vec3(255,255,255),vec3(255, 0, 255),color(214, 5, 239))
    scene.debug:line(vec3(255,255,255),vec3(255, 255, 0),color(5, 238, 239))
    
    scene.debug:line(vec3(255, 255, 0),vec3(255, 0, 0),color(239, 234, 5))
    scene.debug:line(vec3(255,0,255),vec3(255, 0, 0),color(239, 5, 5))
    scene.debug:line(vec3(0,0,255),vec3(255, 0, 255),color(128, 5, 239))
    
    scene.debug:line(vec3(0,255,255),vec3(0, 0, 255),color(239, 5, 172))
    scene.debug:line(vec3(0,255,255),vec3(0, 255, 0),color(185, 239, 5))
    scene.debug:line(vec3(255,255,0),vec3(0, 255, 0),color(5, 238, 239))
end

https://youtu.be/XrQn8YV3UpE

@Bri_G I used the template this time, but I don’t know if it will work.

@binaryblues - nice demo, scrolled around all over the model, including stepping out of the box and no crashing. Thanks.

@Bri_G OK, thank you for the test. I have to think about how I’m going to prepare the next example, the previous example block is all about appearance, and then there’s the interaction, which is a little bit more complicated

@Bri_G OK, thank you for the test. I have to think about how I’m going to prepare the next example, the previous example block is all about appearance, and then there’s the interaction, which is a little bit more complicated

Example10: Details on using the tinted switch

After a lot of research, finally understand the details of the use of the tinted switch. It must be used with the built-in script, where the color is set using statements like this:

    dirt.tinted = true

    function dirt:created()
        local x,y,z = self:xyz()       
        self.voxels:set(x,y,z,"color", color(128,128,128,255))
    end

Then we have to face the problem that if a simple unscripted block sets the tinted switch to true, it doesn’t take into account the block’s original color, which is the color set by this statement:

    dirt.setColor(UP, color(255))

it’s gonna turn black.

I found a way around this by traversing the entire coordinate area and using tinted color for all the blocks in that coordinate area, mainly using this method:

    - The coordinate area is from vec3(0,0,0) to vec3(40,20,128)
    scene.voxels:iterateBounds(0,0,0,40,20,128,  function(x,y,z)
        scene.voxels:set(x,y,z,"color",color(240, 242, 10))
    end)

This is a global setting, because the simple block does not provide a built-in script, so it can not be set by itself, only in external settings, the benefit is to save a lot of resources.

I call them global tinted settings and local tinted settings, respectively, and the example code demonstrates what happens when you use both settings together. Note that I’m only putting local tinted settings in function created() in built-in script, you can try to put it in the blockUpdate(), or you can try to put it in the update() function when the global setting is now in setup()

The code:

viewer.mode = FULLSCREEN

function setup()
    scene = craft.scene()
    -- scene.sky.material.sky = color(80, 203, 233)
    -- scene.sky.material.horizon = color(80, 108, 233)
    
    player = scene.camera:add(OrbitViewer, vec3( 40, 25, 40), 8, 1, 4000)
    c = scene.camera:get(craft.camera)
    c.farPlane = 5000
    
    -- set voxel terrain parameters: size, coord
    scene.voxels:resize(vec3(16,1,16),vec3(64,512,64))      
    scene.voxels.coordinates = vec3(0,1000,0)    
    scene.voxels.visibleRadius=1000
    
    -- Create 2 new block types: "simpleGrass", "simpleDirt1"
    createSimpleBlockType("simpleGrass", "Blocks:Grass Top", color(201, 233, 80))
    
    -- Using block type: simpleGrass as the filling unit
    scene.voxels:fill("simpleGrass")    
    -- Create 2 boxes using "simpleGrass" block type, every unit in the box is a "simpleGrass" block
    scene.voxels:box(0,0,0, 16*4,1,16*8)
    
    scene.voxels:box(0,2,0, 8,255,8)
    
    -- Using new block type: simpleGrass as the filling unit
    scene.voxels:fill("simpleGrass")
    -- A single block
    scene.voxels:block(vec3(30,25,35))
    -- Create a sphere and a line using "simpleGrass" block type, 
    -- every unit in those two shapes is a "simpleGrass" block
    scene.voxels:line(10,20,0,10,30,40)
    scene.voxels:sphere(40,20,60,10)
    -- craft.scene.main = scene
       
    -- Iterate over the block at each location within the region and set its tinted color
    scene.voxels:iterateBounds(0,0,0,40,20,128,  function(x,y,z)
        scene.voxels:set(x,y,z,"color",color(240, 242, 10))
    end)   
end


function createSimpleBlockType(blockTypeName,texture,blockColor)
    -- Create user-defined block type
    scene.voxels.blocks:addAssetPack("Blocks")
    
    local dirt = scene.voxels.blocks:new(blockTypeName)
    ---[[
    dirt.setTexture(ALL, texture or "Blocks:Dirt")
    dirt.setTexture(UP, "Blocks:Dirt")
    dirt.setTexture(DOWN, "Blocks:Stone")
    dirt.setTexture(EAST, "Blocks:Grass2")
    dirt.setTexture(NORTH, "Blocks:Grass1")
    dirt.setTexture(WEST, "Blocks:Leaves")
    dirt.setTexture(SOUTH, "Blocks:Oven")
    --]]
    
    ---[[
    dirt.setColor(ALL, blockColor or color(251))
    dirt.setColor(UP, color(255))
    dirt.setColor(DOWN, color(238, 235, 235))
    dirt.setColor(EAST, color(5, 235, 239))
    dirt.setColor(NORTH, color(249, 248, 248))
    dirt.setColor(WEST, color(249))
    dirt.setColor(SOUTH, color(255))
    --]]
    
    dirt.tinted = true
    dirt.scripted = true
    dirt.state:addFlag("on", false)
    
    function dirt:created()
        -- Randomise colour based on location
        local x,y,z = self:xyz()
        math.randomseed(x * y * z)
        local c =color(math.random(128,255), math.random(128,255), math.random(128,255))
        self.voxels:set(x,y,z,"color", c)
    end
end

function update(dt)
    scene:update(dt)
    blockRange()
end

function draw()
    update(DeltaTime)    
    scene:draw()
end

https://youtu.be/3Bm-CP-EP0o

@John Another problem with the local tinted?Block settings in the built-in script?, as we know, is that the invisible internal block does not need to be handled. There will be a culling algorithm, but there seems to be a problem with the computation. Some blocks are partially visible but also culled, resulting in total darkness.

Test code:

    scene.voxels:box(0,2,0, 8,64,8)
    scene.voxels:box(0,60,0, 12,64,12)

@binaryblues It’s been a long while since I looked at the voxel meshing code, but digging into it I’ve found this

 void BlockModel::appendToMesh(VoxelMesher& mesher, const BlockType& blockType, const Block& block, int index, const glm::ivec3& coord, const Occlusion& occlusion)
    {
        // Tinted block types automatically extract face colors from block state
        glm::lowp_uvec4 tint;
        if (blockType.tinted)
        {
            tint.r = (block.state & 0xFF000000) >> 24;
            tint.g = (block.state & 0x00FF0000) >> 16;
            tint.b = (block.state & 0x0000FF00) >> 8;
            tint.a = (block.state & 0x000000FF);
        }
        
        for (auto& element : _elements)
        {
            for (int i = 0; i < (int)BlockFace::Count; i++)
            {
                BlockElementFace& face = element.face((BlockFace)i);
                
                // Check for culling
                if ((face.cullface != BlockFace::None && occlusion[face.cullface]) ||
                    face.textureIndex == Atlas::ItemNotFound)
                {
                    continue;
                }
                
                if (face.visible)
                {
                    if (blockType.tinted)
                    {
                        face.faceInfo.color = tint;
                    }
                    face.faceInfo.subMesh = (int)blockType.renderPass;
                    mesher.addQuad(index, coord, face.faceInfo);
                }
            }
        }
    }

You can see here that if blockType is tinted then all colors are overridden. You could potentially have some additional options for how tinting works (maybe replace/multiply/add). Another gotcha is that the entire block state (which is 32 bits at the moment) is interpreted as color data when a block is set to tinted. I’m not entirely sure how additional state works when it gets full.

Also the occlusion bug is weird, do you mean the big section of white blocks or is it something else?

@John Thank you so much for taking the time to look at the old code, which is a pain, and I’m sorry that we all know that programmers would rather write new code than look at old code from a few years ago.

I found three problems with the block:

  • 1, when I set the texture for the block, it’ll use north over south, which should be normally used for each texture,
  • 2, just set tinted to true, without any other settings, all blocks will go black, as you can see in the video I posted earlier. Since I wasn’t familiar with the details of tinted, I wasn’t sure if this was a problem. My solution was to add the corresponding processing logic,
  • 3, the blocks inside should theoretically be removed when tinted, because they are invisible to the user. But there are problems with the border processing, and some blocks that have one or both faces on the outside are also removed, showing black on my device, but the images from the Codea screenshots show large white areas. Please see the picture below.

For the third problem, I experimented with the global tinted setting, using the following statement, which turned out to be correct.

--  This can be handled correctly
    scene.voxels:iterateBounds(x1,y1,z1,x2,y2,z2,  function(x,y,z)       
        scene.voxels:set(x,y,z,"color",color(240, 242, 10))
    end)   

Only the block that was tinted with the built-in script had a problem.

And on the third question, I made a guess, and I noticed that voxels’starting point is (0,0,0) , and I saw that it starts with the subscript 0 when it’s looping, it’s a bit different from the way Lua always starts with 1, for example, if we want to traverse a 256 * 256 *256 region, we would say 0 to 255 instead of 1 to 256, and maybe that’s the inconsistency, of course, this is just a guess. I hope it helps. Thanks again.

This code will create a black zone

    -- Test box
    scene.voxels:box(0,2,0, 8,64,8)
    scene.voxels:box(0,60,0, 12,64,12)

The black zone demo?

https://youtu.be/OVdJ_udgTds

Change the range of the 2nd box, add one unit to the minimum of X and one unit to the minimum of Z, and subtract one unit from the maximum of Y, and you will see the image processed correctly

    -- Test box
    scene.voxels:box(0,2,0, 8,64,8)
    scene.voxels:box(0+1,60,0+1, 12,64-1,12)

The demo

https://youtu.be/M-IrwdpmeXg

The below is the correct picture:

Ah I see what you mean. I don’t think those black voxels are being culled because otherwise you’d be able to see though them, but they are definitely black. I think the reason why you get a default block showing up as black after having tinted enabled is that all block state is 0 by default, which decodes as black when interpreted as a color (even if you have a texture since 0 x color = 0).

What do you think the default behaviour should be in this case?

@John Hi, Thanks for the answer. Are you talking about the second problem I found I feel like you’re treating the second question and the third question as if they’re the same question, and the second question doesn’t involve elimination.

Let’s start with the second question, which is about default behavior, and I think it’s okay that they’re black. According to your description, the logic of tinted in the code should come last, so its processing will produce the final visual effect if the tinted color is set in the built-in script, it will be calculated based on the tinted color of the settings, and if the settings are black, it will handle it correctly; if no tinted color is set in the built-in script, then it will also use black (state 0, will be decoded to black) for computation. This is done correctly, because after thinking about it for a while, it seems to me that we don’t really need to distinguish between the two states exactly: tinted turned on, color set to black, VS. tinted turned on, and no color set.

The only thing we might need to do, perhaps, is add a little documentation about tinted to remind the user that if you turn the tinted switch on but don’t set the tinted color in the built-in script, it will appear black.

These are three separate problems, Now we’ve solved the second one, and I’ll list them again:

  • 1st issue: when I set the texture for the block, it’ll use north over south, which should be normally used for each texture.
  • [Solved] 2nd issue: just set tinted to true, without any other settings, all blocks will go black, as you can see in the video I posted earlier. Since I wasn’t familiar with the details of tinted, I wasn’t sure if this was a problem. My solution was to add the corresponding processing logic.
  • 3rd issue: I’ve re-examined the third problem, and now that I’ve found the explicit trigger, I’ve created two new boxes with partially overlapping shapes. Tinted is enabled and the tinted colors are set correctly in the built-in script. Now that the tinted calculations for the overlapping parts are not accurate, Codea culls out the exposed blocks and set it to black.

Btw. I found the default fillStyle is 0 (REPLACE)

you can use this to see the result:

    -- Test box have the same y, 
    scene.voxels:box(0,60,0, 8,64,8)
    scene.voxels:box(0,60,0, 12,64,12)

https://youtu.be/j7iKmKDnqKQ

Below is the whole test project

Example 11 Music block

The previous examples are all about the appearance of block, and now it’s finally time to interact with block. After a lot of reference to Codea’s built-in examples, I decided not to use too much of the abstraction layer for the time being because it took me a while to read the code myself, John’s code architecture was good, and modules were highly cohesive, low coupling. But for starters, you have to jump from one project to another to get a general picture. It is recommended that beginners learn the built-in sample code after they have a basic understanding of craft. After all, most of what I talk about in this article is learned from the built-in sample program code.

So my example will try to break it down and minimize the level of abstraction.

This example is a music box, click the first time it will start playing music, click the second time it will stop playing, the main method used is:

    function soundb:interact()
        local on = self:get("on")
        print("on:", on)
        if on then                
            local x,y,z = self:xyz()
            speech.say("music")
            sound(SOUND_RANDOM, x * y * z)
            music(asset.downloaded.A_Hero_s_Quest.In_the_City)
            self:set("on", false)
        else
            speech.say("Good bye")
            music.stop()
            self:set("on", true) 
        end        
    end 

And raycast judging, as well as touch handling, the whole code as follows:

viewer.mode = FULLSCREEN

function setup()
    scene = craft.scene()
    scene.sky.material.sky = color(80, 203, 233)
    scene.sky.material.horizon = color(80, 108, 233)

    player = scene.camera:add(OrbitViewer, vec3( 35, 25, 0), 1, 0, 4000)
    player.rx = 15
    c = scene.camera:get(craft.camera)
    c.farPlane = 5000
    
    -- set voxel terrain parameters: size, coord
    scene.voxels:resize(vec3(16,1,16),vec3(64,256,64))      
    scene.voxels.coordinates = vec3(0,0,0)    
    scene.voxels.visibleRadius=1000
    
    -- Create 2 new block types: "simpleGrass", "simpleDirt1"
    createSimpleBlockType("simpleGrass", textures, blockColors)
    -- Using block type: simpleGrass as the filling unit
    scene.voxels:fill("simpleGrass")    
    -- Create 2 boxes using "simpleGrass" block type, every unit in the box is a "simpleGrass" block
    scene.voxels:box(0,0,0, 16*4,1,16*4)
    
    ---[[ Using a new block type: Music, which can play music
    newBlockTypeInteract("Music")
    scene.voxels:fill("Music")    
    -- A single block
    scene.voxels:block(vec3(30,25,15))
    myRoom(35,20,20, 36,30,20)  
    --]]  
end


function newBlockTypeInteract(blockTypeName)
    -- scene.voxels.blocks:addAssetPack("Blocks")
    -- Create user-defined block type
    local soundb = scene.voxels.blocks:new(blockTypeName)
    soundb.setTexture(ALL, "Blocks:Dirt")
    soundb.setColor(ALL, color(239, 222, 5))
    soundb.tinted = true
    soundb.scripted = true
    soundb.state:addFlag("on", true)
    
    -- The internal script
    function soundb:created()
        self:schedule(60)
        local c =color(math.random(128,255), math.random(128,255), math.random(128,255))
        self:set(COLOR, c)
    end
    
    function soundb:blockUpdate()
        local on = self:get("on")
        if on then
            local c =color(math.random(128,255), math.random(128,255), math.random(128,255))
            self:set(COLOR, c)
        end
        self:schedule(10)
    end
    
    function soundb:interact()
        local on = self:get("on")
        print("on:", on)
        if on then                
            local x,y,z = self:xyz()
            speech.say("music")
            sound(SOUND_RANDOM, x * y * z)
            music(asset.downloaded.A_Hero_s_Quest.In_the_City)
            self:set("on", false)
        else
            speech.say("Good bye")
            music.stop()
            self:set("on", true) 
        end        
    end 

    return soundb
end

function update(dt)
    scene:update(dt)
    blockRange()
end

function draw()
    update(DeltaTime)    
    scene:draw()
end

function touched(touch)    
    -- The OrbitViewer's touched handler
    touches.touched(touch) 
    
    -- Our block 
    if touch.state == BEGAN then
        -- print("touch: ",touch)
        local blockCoord, blockID, blockFace = raycast(touch.x, touch.y)
        print(blockCoord)
        if blockID and blockID ~= 0 then
            local touchedBlock =  scene.voxels:get(blockCoord)
            print("touchedBlock : ", touchedBlock)  
            if touchedBlock.interact then
                touchedBlock:interact()
            end
        end
    end
end

-- helper function
function raycast(x,y, sides)
    local blockID = nil
    local blockCoord = nil
    local blockFace = nil
    -- Use the main camera to get the position and direction for the raycast from a screen location
    local origin, dir = scene.camera:get(craft.camera):screenToRay(vec2(x, y))
    
    scene.voxels:raycast(origin, dir, 5000, function(coord, id, face)
        if coord and id ~= 0 then
            blockID = id
            blockCoord = coord
            blockFace = face
            return true
        end
        return false
    end)  
    return blockCoord, blockID, blockFace
end

This time there’s a strange problem, when you’re on one side of the block, clicking on the block won’t work. Clicking on the block normally triggers the music only by rotating the screen to the other side of the block.

I got the reason, For Codea voxels system, all of its valid ranges must be above 0, that is, all of its coordinates must be positive, and it’s in a big box from vec3(0,0,0) to vec3(maxX,maxY,maxZ), like a sandbox. Accordingly, the location coordinates of the camera can not have negative numbers, otherwise it will be considered invalid and will not be processed. This is a very clear system constraint. Maybe I missed it when I was looking at the built-in documentation.

https://youtu.be/dY9X8IOalhU

The final version!

All the sample programs have been tidied up today, integrated into a project, and added a few instructions that hopefully will help novices get a quick read on the voxels system

https://youtu.be/fbkeRB2UWlA

There was a problem with the integration of my touch code with the OrbitViewer’s touch code, which sometimes caused the touch to fail. All you have to do is click the restart button(the circle arrow)