Cheat Engine Forum Index Cheat Engine
The Official Site of Cheat Engine
 
 FAQFAQ   SearchSearch   MemberlistMemberlist   UsergroupsUsergroups   RegisterRegister 
 ProfileProfile   Log in to check your private messagesLog in to check your private messages   Log inLog in 


Drawing a 3D Cube with CE Lua [GDI+]

 
Post new topic   Reply to topic    Cheat Engine Forum Index -> Cheat Engine Tutorials -> LUA Tutorials
View previous topic :: View next topic  
Author Message
AylinCE
Grandmaster Cheater Supreme
Reputation: 37

Joined: 16 Feb 2017
Posts: 1516

PostPosted: Tue Jul 29, 2025 7:49 pm    Post subject: Drawing a 3D Cube with CE Lua [GDI+] Reply with quote

More Explanatory and Detailed:

Cheat Engine Lua: A Fully Transparent 3D Cube Rendering Module Using GDI+

Create Your Own 3D Scene in Cheat Engine: Multi-Cube Drawing and Animation

Advanced GDI+ Usage: Modular 3D Primitive Rendering in Cheat Engine



---------------------------------------------------------------------------------

It is an expandable module where you can draw and move one or more cubes in the same or separate picture frames.

------------ Description: ------------------------------------------------------

GDI+ and fillPolygon Processing in Cheat Engine Lua
The Lua environment provided by Cheat Engine provides direct access to the Windows GDI+ (Graphics Device Interface Plus) graphics library. GDI+ is an API for 2D vector graphics, image processing, and typography. In Cheat Engine, we access GDI+ functionality through the Picture.Bitmap.Canvas property of the TImage control.

1. The Relationship Between TImage and Bitmap.Canvas:

TImage Control: The UI component that represents the visual area you see on your Cheat Engine form. It has its own Width, Height, and Picture properties.

Picture.Bitmap: The image data displayed within the TImage control. It is a bitmap composed of pixels. The bitmap also has its own dimensions, such as Width and Height.

Bitmap.Canvas: This is the key. Bitmap.Canvas is a "drawing surface" that allows you to draw on this bitmap. Most GDI+ functions are called through this Canvas object. The Canvas includes tools such as the Pen (for pen, lines) and the Brush (for brush, fills).

2. The Role of the fillPolygon Function:

fillPolygon is not a direct GDI+ or Cheat Engine built-in function. It is a special helper function that fills a complex shape (polygon) that we create in Lua using GDI+ primitive functions (such as MoveTo, LineTo, Pen.Color, Brush.Color, FillRect, etc.).

How fillPolygon Works (Scanline Algorithm Logic):

Our fillPolygon function typically uses a simplified version of a "scanline" algorithm:

Boundary Box Calculation: The function first determines the outermost boundaries (minimum X, maximum X, minimum Y, maximum Y) of the given set of points. This optimizes the area to be drawn.

Horizontal Scan Lines: A loop is then started for each pixel line (y) from this minimum Y to the maximum Y.

Intersection Points: Each y-line contains the intersection points with the edges of the polygon. If an edge intersects the y-line, the X coordinate of this intersection is calculated and added to a list (intersections).

Sorting: The list of intersection points is sorted by the X coordinate.

Pair Filling: The sorted intersection points are grouped into two. The first point is assigned to x1, and the second point is assigned to x2. The horizontal line between these two X coordinates is drawn by calling canvas.MoveTo(x1, y) and canvas.LineTo(x2, y) and filled with Brush.Color. This process is repeated for all intersection pairs.

This method efficiently fills the interior of complex shapes by drawing simple horizontal lines.

3. Z-Order and Painter's Algorithm:

Drawing objects in the correct order is crucial when creating a 3D scene. Since Cheat Engine lacks hardware-accelerated 3D features like a depth buffer (Z-buffer), we use the Painter's Algorithm.

Rationale: Draw the farthest objects or surfaces first, then draw closer objects over them. This way, closer objects automatically cover the hidden parts of the farther objects.

Implementation: In our module, the average Z value (avgZ) of each face of each cube is calculated. All faces are sorted from largest to smallest according to this avgZ value (a.avgZ > b.avgZ). Faces with larger Z values (those farther away) are drawn first.

_transparentColor - 1 "Trick": As you can observe, there are some behavioral nuances regarding the TImage.TransparentColor property. Normally, pixels with the color you set to Bitmap.TransparentColor are expected to be interpreted as transparent by the TImage control. However, in practice, sometimes this color may not behave transparently or may exhibit unwanted artifacts, even if it directly matches the form's background color. Using a color slightly different from the form's background color, such as transparentColor - 1, has served as a "hack" or "workaround" by ensuring that Cheat Engine's GDI+ implementation consistently recognizes this color as transparent.

4. Drawing Cycle (drawAll()):

Every frame (when the timer.OnTimer event is triggered):

self._canvas.Clear(): Clears all drawing from the previous frame. This is crucial for animation.

Background Cube Exclusion: Faces of the background cube marked with the isInternalBackgroundCube flag and with the actualTransparentColorForBitmap color are not added to the allSortedFaces list. This ensures that those faces are never drawn, leaving the background of the Image control transparent.

Face Sorting: All remaining cube faces (excluding the background cube) are sorted based on their avgZ values.

Face Drawing: The sorted faces are drawn using the fillPolygon function. The color of each face is set to faceColor.

Edge Drawing: The edges of each filled face are drawn by setting Pen.Color = 0x000000 (black), giving the cubes a sense of 3D depth.

This combination allows us to create highly effective 3D renderings even within the limited graphics capabilities of Cheat Engine.

*********************************************************
*********************************************************
*********************** MODULE **************************

Code:
-- CubeRendererModule.lua
-- Author by :  AylinCE
-- This module provides a constructor to create CubeRenderer instances,
-- each managing 3D cube rendering on a specific TImage control.

local CubeRenderer = {}
CubeRenderer.__index = CubeRenderer

-- Private helper functions (shared by all instances)
local function fillPolygon(canvas, points, color)
    local numPoints = #points
    if numPoints < 3 then return end

    local minX = points[1].x
    local maxX = points[1].x
    local minY = points[1].y
    local maxY = points[1].y

    for i = 1, numPoints do
        minX = math.min(minX, points[i].x)
        maxX = math.max(maxX, points[i].x)
        minY = math.min(minY, points[i].y)
        maxY = math.max(maxY, points[i].y)
    end

    canvas.Pen.Color = color
    canvas.Brush.Color = color

    for y = math.floor(minY), math.ceil(maxY) do
        local intersections = {}

        for i = 1, numPoints do
            local p1 = points[i]
            local p2 = points[(i % numPoints) + 1]

            if (p1.y <= y and p2.y > y) or (p2.y <= y and p1.y > y) then
                local slope = (p2.x - p1.x) / (p2.y - p1.y)
                local intersectX = p1.x + (y - p1.y) * slope
                table.insert(intersections, intersectX)
            end
        end

        table.sort(intersections, function(a, b) return a < b end)

        for i = 1, #intersections, 2 do
            local x1 = math.floor(intersections[i])
            local x2 = math.ceil(intersections[i+1])

            if x1 and x2 and x1 < x2 then
                canvas.MoveTo(x1, y)
                canvas.LineTo(x2, y)
            end
        end
    end
end

local function rotatePoint(p, rotX, rotY)
    local yRot = p[2] * math.cos(math.rad(rotX)) - p[3] * math.sin(math.rad(rotX))
    local zRot = p[2] * math.sin(math.rad(rotX)) + p[3] * math.cos(math.rad(rotX))
    local xRot = p[1]

    local xFinal = xRot * math.cos(math.rad(rotY)) - zRot * math.sin(math.rad(rotY))
    local zFinal = xRot * math.sin(math.rad(rotY)) + zRot * math.cos(math.rad(rotY))
    local yFinal = yRot

    return {xFinal, yFinal, zFinal}
end

---
-- Constructor for CubeRenderer instances.
-- @param imageControl The TImage control where the cubes will be drawn.
-- @param transparentColor The color to be used for transparency in the image bitmap.
--        This should ideally be the same as the parent form's background color.
-- @return A new CubeRenderer instance.
function CubeRenderer.new(imageControl, transparentColor)
    local self = setmetatable({}, CubeRenderer)

    self._boxes = {}            -- Cubes for this specific renderer instance
    self._parentImage = imageControl
    self._transparentColor = transparentColor -- Formun arka plan rengi
    self.globalAngleX = 0       -- Global rotation for this instance's cubes
    self.globalAngleY = 0

    if not self._parentImage or not self._parentImage.Picture or not self._parentImage.Picture.Bitmap then
        error("CubeRenderer.new - Invalid imageControl provided. Must be a TImage with a valid bitmap.")
    end

    local bm = self._parentImage.Picture.Bitmap
    bm.Width = self._parentImage.Width
    bm.Height = self._parentImage.Height

    -- CRITICAL: Use a slightly different color for bitmap transparency if direct match fails.
    -- This relies on your previous observation that transparentColor - 1 worked.
    local actualTransparentColorForBitmap = transparentColor - 1
    bm.TransparentColor = actualTransparentColorForBitmap
    self._parentImage.Transparent = true -- Ensure the image control itself uses its transparent color

    self._canvas = bm.Canvas
    self._centerX = bm.Width / 2
    self._centerY = bm.Height / 2

    -- Background Cube Logic RE-INTRODUCED for stable transparent background.
    -- This cube will always be drawn first (at the back) and its faces will be
    -- set to the actualTransparentColorForBitmap, making it transparent.
    local bgCube = self:create(
        0, 0, 1000, -- Large positive Z to ensure it's always at the very back
        self._parentImage.Width * 2, self._parentImage.Height * 2, 1, -- Large dimensions, very thin
        {actualTransparentColorForBitmap-1, actualTransparentColorForBitmap, actualTransparentColorForBitmap,
         actualTransparentColorForBitmap, actualTransparentColorForBitmap, actualTransparentColorForBitmap-1}
    )
    bgCube.isInternalBackgroundCube = true -- Mark this cube as internal
    bgCube.canMove = false -- Internal background cube should not be user-movable
    bgCube.canRotate = false -- Internal background cube should not be user-rotatable
    -- Insert at the beginning of the list to ensure it's processed early for background.
    table.insert(self._boxes, 1, bgCube)

    return self
end

---
-- Creates a new 3D cube and adds it to this renderer instance.
-- @param x X-coordinate of the cube's center.
-- @param y Y-coordinate of the cube's center.
-- @param z Z-coordinate of the cube's center. (Larger Z means farther from camera in this implementation)
-- @param width Width of the cube.
-- @param height Height of the cube.
-- @param depth Depth of the cube.
-- @param colors (Optional) Table of 6 colors for the cube's faces (back, front, left, right, bottom, top).
--               If nil, default colors will be used.
-- @return The created cube object (table) with properties like position, dimensions, angleX, angleY.
function CubeRenderer:create(x, y, z, width, height, depth, colors)
    local newBox = {
        position = {x, y, z},
        dimensions = {width, height, depth},
        colors = colors or {
            0xFF0000, -- Default Red
            0x00FF00, -- Default Green
            0x0000FF, -- Default Blue
            0xFFFF00, -- Default Yellow
            0xFF00FF, -- Default Magenta
            0x00FFFF  -- Default Cyan
        },
        angleX = 0, -- Individual rotation for this cube
        angleY = 0
    }

    -- Get the *actual* transparent color used by the bitmap
    local actualTransparentColorForBitmap = self._parentImage.Picture.Bitmap.TransparentColor

    -- Check if any cube color is identical to actualTransparentColorForBitmap, which would make it invisible.
    for i, color in ipairs(newBox.colors) do
        if color == actualTransparentColorForBitmap then
            --print(string.format("[WARNING] CubeRenderer:create - Cube color 0x%X (index %d) is identical to the bitmap's TransparentColor (0x%X). This face will be invisible.", color, i, actualTransparentColorForBitmap))
        end
    end
    -- Also warn if a face is black, as edges are black
    for i, color in ipairs(newBox.colors) do
        if color == 0x000000 then
             --print(string.format("[WARNING] CubeRenderer:create - Cube face color (index %d) is black (0x000000), which is also the default edge color. This may hide edges.", i))
        end
    end

    -- Calculate base vertex coordinates relative to the cube's center
    newBox.basePoints = {
        {x - width/2, y - height/2, z - depth/2}, -- 1
        {x + width/2, y - height/2, z - depth/2}, -- 2
        {x + width/2, y + height/2, z - depth/2}, -- 3
        {x - width/2, y + height/2, z - depth/2}, -- 4

        {x - width/2, y - height/2, z + depth/2}, -- 5
        {x + width/2, y - height/2, z + depth/2}, -- 6
        {x + width/2, y + height/2, z + depth/2}, -- 7
        {x - width/2, y + height/2, z + depth/2}  -- 8
    }

    -- Define faces using vertex indices
    newBox.faces = {
        {1, 4, 3, 2, newBox.colors[1]}, -- Z- (back)
        {5, 6, 7, 8, newBox.colors[2]}, -- Z+ (front)
        {1, 5, 8, 4, newBox.colors[3]}, -- X- (left)
        {2, 3, 7, 6, newBox.colors[4]}, -- X+ (right)
        {1, 2, 6, 5, newBox.colors[5]}, -- Y- (bottom)
        {4, 8, 7, 3, newBox.colors[6]}  -- Y+ (top)
    }

    -- User-created cubes are inserted at the end of the list.
    -- The internal background cube is always at index 1.
    table.insert(self._boxes, newBox)

    return newBox
end

---
-- Draws all cubes associated with this renderer instance to its parent image's canvas.
-- This function should be called repeatedly for animation.
function CubeRenderer:drawAll()
    if not self._canvas then
        error("CubeRenderer:drawAll - Instance not properly initialized.")
    end

    self._canvas.Clear() -- Clear the canvas before drawing anything.

    local allSortedFaces = {}

    local actualTransparentColorForBitmap = self._parentImage.Picture.Bitmap.TransparentColor

    for boxIndex, box in ipairs(self._boxes) do
        local rotatedPointsForThisBox = {}
        for i, point in ipairs(box.basePoints) do
            local rotX_applied = box.angleX
            local rotY_applied = box.angleY

            -- Apply global rotation ONLY if it's not the internal background cube.
            if not box.isInternalBackgroundCube then
                rotX_applied = rotX_applied + self.globalAngleX
                rotY_applied = rotY_applied + self.globalAngleY
            end

            local rotatedPt = rotatePoint(point, rotX_applied, rotY_applied)
            table.insert(rotatedPointsForThisBox, rotatedPt)
        end

        for faceIndex, faceData in ipairs(box.faces) do
            -- If this is the background cube AND its face color is the transparent color,
            -- do NOT include it in the sorted list to prevent drawing.
            -- This is the "silme" (erasing) part.
            if box.isInternalBackgroundCube and faceData[5] == actualTransparentColorForBitmap then
                -- Skip this face, it's meant to be transparent background
            else
                local sumZ = 0
                -- Get average Z of face for sorting
                for j = 1, 4 do
                    local idx = faceData[j]
                    sumZ = sumZ + rotatedPointsForThisBox[idx][3]
                end
                table.insert(allSortedFaces, {
                    avgZ = sumZ / 4,
                    faceData = faceData,
                    rotatedPoints = rotatedPointsForThisBox -- Store rotated points for this specific face
                })
            end
        end
    end

    -- Sort all faces based on their average Z value in DESCENDING order (larger Z means farther, draw first).
    table.sort(allSortedFaces, function(a, b) return a.avgZ > b.avgZ end)

    -- Draw sorted faces
    for _, sortedFaceInfo in ipairs(allSortedFaces) do
        local faceData = sortedFaceInfo.faceData
        local rotatedPoints = sortedFaceInfo.rotatedPoints
        local faceColor = faceData[5]

        -- If a face's color is the actual transparent color, we skip drawing it.
        -- This covers the background cube's faces which should be transparent.
        if faceColor == actualTransparentColorForBitmap then
            -- Do nothing, this area should remain transparent.
        else
            local screenPoints = {}
            for j = 1, 4 do
                local idx = faceData[j]
                local pt = rotatedPoints[idx]
                -- Project 3D point to 2D screen coordinates
                table.insert(screenPoints, {
                    x = self._centerX + pt[1],
                    y = self._centerY - pt[2] -- Y-axis inverted for screen coordinates
                })
            end

            -- Fill face polygon
            fillPolygon(self._canvas, screenPoints, faceColor)

            -- Draw edges
            self._canvas.Pen.Color = 0x000000 -- Black edges
            self._canvas.MoveTo(screenPoints[1].x, screenPoints[1].y)
            for j = 2, 4 do
                self._canvas.LineTo(screenPoints[j].x, screenPoints[j].y)
            end
            self._canvas.LineTo(screenPoints[1].x, screenPoints[1].y) -- Connect last point to first
        end
    end
end

---
-- Clears all user-created cubes associated with this renderer instance.
-- The internal background cube is retained.
function CubeRenderer:clearCubes()
    local newBoxes = {}
    -- Keep the first cube if it's the internal background cube
    if self._boxes[1] and self._boxes[1].isInternalBackgroundCube then
        table.insert(newBoxes, self._boxes[1])
    end
    self._boxes = newBoxes
end

---
-- Retrieves a specific user-created cube by its 1-based index from this instance.
-- @param index The 1-based index of the user-created cube.
-- @return The cube object (table) or nil if not found.
function CubeRenderer:getCube(index)
    local offset = 0
    if self._boxes[1] and self._boxes[1].isInternalBackgroundCube then
        offset = 1 -- If background cube exists, user cubes start from index 2 in _boxes
    end
    return self._boxes[index + offset]
end

---
-- Returns the total number of user-created cubes associated with this instance.
function CubeRenderer:getCubeCount()
    local count = #self._boxes
    if self._boxes[1] and self._boxes[1].isInternalBackgroundCube then
        count = count - 1 -- Subtract 1 for the internal background cube
    end
    return count
end

--return CubeRenderer -- Return the module table (which is a constructor in this case)
------------------------------------------------------


*********************************************************
*********************************************************
******************* Example GUI usage *********************

Code:
-- --- Form Setup ---
if form then form.Destroy() form = nil end
form = createForm()
form.Caption ="Çoklu Küp Çizimi (Modüler)"
form.Width = 1000 -- Wider form for multiple images
form.Height = 700

local FORM_BACKGROUND_COLOR = 0xF0F0F0 -- Light gray
form.Color = FORM_BACKGROUND_COLOR

-- --- Image 1 Setup ---
local image1 = createImage(form)
image1.Width = 450
image1.Height = 450
image1.Left = 20
image1.Top = 80
image1.Transparent = true
image1.Picture.Bitmap.TransparentColor = FORM_BACKGROUND_COLOR

-- Create a CubeRenderer instance for image1
local renderer1 = CubeRenderer.new(image1, FORM_BACKGROUND_COLOR)

-- Create cubes for image1
local cube1_1 = renderer1:create(0, 0, 0, 130, 8, 20) -- Main "pen" cube
local cube1_2 = renderer1:create(-100, 50, 0, 40, 40, 40, {
    0xAAAAAA, 0x888888, 0x666666, 0x444444, 0x222222, 0x111111 -- Gray tones
})
local cube1_3 = renderer1:create(100, -50, 0, 80, 20, 80, {
    0x0000FF, 0xFF0000, 0x00FF00, 0x0000FF, 0xFF0000, 0x00FF00 -- Red, Blue, Green
})

-- --- Image 2 Setup ---
local image2 = createImage(form)
image2.Width = 450
image2.Height = 450
image2.Left = 520 -- Position next to image1
image2.Top = 80
image2.Transparent = true
image2.Picture.Bitmap.TransparentColor = FORM_BACKGROUND_COLOR

-- Create a CubeRenderer instance for image2
local renderer2 = CubeRenderer.new(image2, FORM_BACKGROUND_COLOR)

-- Create cubes for image2
local cube2_1 = renderer2:create(0, 0, 0, 80, 80, 80, {
    0xFF00FF, 0x00FFFF, 0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00 -- Different colors
})
local cube2_2 = renderer2:create(0, 100, -50, 60, 20, 100, {
    0x00FF00, 0xFFFFFF, 0x0000FF, 0xFF0000, 0x00FFFF, 0xFF00FF -- Mixed colors, one white
})

-- --- Animation Logic ---
local timer
local isAnimating = false

-- Animation update function
local function animateAllRenderers()
    -- Update global angles for renderer1
    renderer1.globalAngleX = (renderer1.globalAngleX + 1) % 360
    renderer1.globalAngleY = (renderer1.globalAngleY + 1) % 360

    -- Update global angles for renderer2 (different speed/direction)
    renderer2.globalAngleX = (renderer2.globalAngleX - 2) % 360 -- Rotate opposite direction
    renderer2.globalAngleY = (renderer2.globalAngleY + 0.5) % 360 -- Slower rotation

    -- Rotate a specific cube in renderer1 individually
    if cube1_3 then
        cube1_3.angleX = (cube1_3.angleX + 3) % 360
        cube1_3.angleY = (cube1_3.angleY + 1) % 360
    end

    -- Rotate a specific cube in renderer2 individually
    if cube2_1 then
        cube2_1.angleX = (cube2_1.angleX + 2) % 360
    end
     if cube2_2 then
        cube2_2.angleY = (cube2_2.angleY + 5) % 360
    end
    -- Draw all cubes for each renderer
    renderer1:drawAll()
    renderer2:drawAll()
end

-- --- UI Elements ---
local rotateButton = createButton(form)
rotateButton.Caption = "Start Animation"
rotateButton.Left = (form.Width - rotateButton.Width) / 2
rotateButton.Top = 10
rotateButton.Width = 150
rotateButton.Height = 30

rotateButton.OnClick = function()
    if not isAnimating then
        timer = createTimer(form)
        timer.Interval = 20 -- 50 FPS
        timer.OnTimer = animateAllRenderers
        timer.Enabled = true
        rotateButton.Caption = "Stop Animation"
        isAnimating = true
        --print("Animation started.")
    else
        if timer then
            timer.Enabled = false
            timer.destroy()
            timer = nil
        end
        rotateButton.Caption = "Start Animation"
        isAnimating = false
        --print("Animation stopped.")
    end
end

-- --- Cleanup on Script Disable ---
function onDisable()
    if form then
        form.destroy()
        form = nil
    end
    if timer then
        timer.destroy()
        timer = nil
    end
    -- Renderer instances will be garbage collected when the script ends or references are lost.
end

-- --- Initial Display ---
form.Show()
renderer1:drawAll() -- Initial draw for renderer1
renderer2:drawAll() -- Initial draw for renderer2



*********************************************************
*********************************************************

I'd love to see how far you go with this idea and what you can do.
As always, enjoy! Until I see you in another crazy article!

*********************************************************
*********************************************************

_________________
Hi Hitler Different Trainer forms for you!
https://forum.cheatengine.org/viewtopic.php?t=619279
Enthusiastic people: Always one step ahead
Do not underestimate me Master: You were a beginner in the past
Back to top
View user's profile Send private message Visit poster's website MSN Messenger
Display posts from previous:   
Post new topic   Reply to topic    Cheat Engine Forum Index -> Cheat Engine Tutorials -> LUA Tutorials All times are GMT - 6 Hours
Page 1 of 1

 
Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum
You cannot attach files in this forum
You cannot download files in this forum


Powered by phpBB © 2001, 2005 phpBB Group

CE Wiki   IRC (#CEF)   Twitter
Third party websites