AylinCE Grandmaster Cheater Supreme
Reputation: 37
Joined: 16 Feb 2017 Posts: 1516
|
Posted: Tue Jul 29, 2025 7:49 pm Post subject: Drawing a 3D Cube with CE Lua [GDI+] |
|
|
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!
*********************************************************
*********************************************************
_________________
|
|