I am currently working on a custom three-dimensional rendering engine using Frames to represent lines in wireframe representations of objects and ImageLabels to represent triangles comprising the surfaces thereof (these GuiObjects are all descendants of a ScreenGui). Specifically, each triangle that composes the surfaces of objects rendered using this engine should, after its vertices are converted into screen coordinates, be (for lack of a better term) “split” into two right triangles based on the location of the altitude line drawn from its longest edge (henceforth referred to as the “base”) to the opposite vertex, as shown in the following example.
The triangles formed thereby will then be filled with ImageLabels containing an image of a right triangle to create the appearance of a surface of a three-dimensional object.
Currently, the ImageLabels are not given the correct Size or Rotation by the engine (Edit: by “engine,” I am referring to the custom rendering engine that I am attempting to create) when rendered and thus do not create the appearance of a surface of a three-dimensional object. The following is a video depicting their current, undesired behavior (note that the correctly rendered wireframe illustrates the intended surfaces of the part).
The relevant portions of the LocalScript responsible for rendering are as follows:
function getValue(list, criterion) --Given a table of numbers, returns the minimum, maximum, or "center" value therein, as well as the index or key at which it is located
if criterion ~= "center" then
local foundValue, foundKey = nil, nil
for key, value in pairs(list) do
if criterion == "max" then
if (not foundValue) or value > foundValue then
foundValue = value
foundKey = key
end
elseif criterion == "min" then
if (not foundValue) or value < foundValue then
foundValue = value
foundKey = key
end
end
end
return foundValue, foundKey
else --The "center" value is the value nearest to the average of all numbers in the table
local sum = 0
local numValues = 0
for key, value in pairs(list) do
sum += value
numValues += 1
end
local average = sum / numValues
local closestValue, foundKey, actualValue = nil, nil, nil
for key, value in pairs(list) do
if (not closestValue) or math.abs(value - average) < closestValue then
closestValue = math.abs(value - average)
actualValue = value
foundKey = key
end
end
return actualValue, foundKey
end
end
function getRotation(point1, point2) --Returns the rotation (in degrees) necessary for a GuiObject to form a line segment connecting the two given points
return math.deg(math.atan((point2.Y - point1.Y) / (point2.X - point1.X)))
end
function drawTriangle(a: Vector3, b: Vector3, c: Vector3, outlineWidth: number) --This is called every frame
outlineWidth = outlineWidth or 2
local surfaceColor = Color3.new(1, 1, 1)
--The following five lines are not relevant
if outlineWidth > 0 then
drawLine(a, b, outlineWidth)
drawLine(a, c, outlineWidth)
drawLine(b, c, outlineWidth)
end
--Convert triangle vertices to screen coordinates
local a2d = camera:WorldToViewportPoint(a)
local b2d = camera:WorldToViewportPoint(b)
local c2d = camera:WorldToViewportPoint(c)
local vertices = {
["a"] = Vector2.new(a2d.X, a2d.Y),
["b"] = Vector2.new(b2d.X, b2d.Y),
["c"] = Vector2.new(c2d.X, c2d.Y)
}
--Determine side lengths of the triangle on-screen
local sideLengths = {
["ac"] = (a2d - c2d).Magnitude,
["ab"] = (a2d - b2d).Magnitude,
["bc"] = (b2d - c2d).Magnitude
}
--Identify the base of the altitude and the hypotenuses of the resulting right triangles
local baseLength, baseKey = getValue(sideLengths, "max")
--The terms "short" and "long" used in some variable names are used solely to identify the triangle of interest and otherwise hold no real significance
local shortHypotenuseLength, shortHypotenuseKey = getValue(sideLengths, "min")
local longHypotenuseLength, longHypotenuseKey = getValue(sideLengths, "center")
local function getVertices(key) --Returns the vertices connected by the given line segment
return vertices[string.sub(key, 1, 1)], vertices[string.sub(key, 2, 2)]
end
--Calculate altitude
local semiperimiter = (sideLengths.ac + sideLengths.ab + sideLengths.bc) / 2
local area = math.sqrt(semiperimiter * (semiperimiter - sideLengths.ab) * (semiperimiter - sideLengths.bc) * (semiperimiter - sideLengths.ac)) --Heron's formula
local altitude = 2 * (area / baseLength)
local function createSurfaceImage() --Create the triangle ImageLabel.
local surfaceImage = Instance.new("ImageLabel")
surfaceImage.Name = "SurfaceColor"
surfaceImage.AnchorPoint = Vector2.new(0.5, 0.5)
surfaceImage.BackgroundTransparency = 1
surfaceImage.ImageColor3 = surfaceColor
surfaceImage.Image = "rbxassetid://10555810706"
return surfaceImage
end
--Identify the vertex opposite the base of the altitude
local baseVertex1, baseVertex2 = getVertices(baseKey)
local vertexOppositeBase = nil
for key, value in pairs(vertices) do
if value ~= baseVertex1 and value ~= baseVertex2 then
vertexOppositeBase = value
break
end
end
--Henceforth, "surfaceImage" refers to an ImageLabel created by the "createSurfaceImage" function
--Identify the vertices connected by the "short hypotenuse"
local shortVertex1, shortVertex2 = getVertices(shortHypotenuseKey)
local surfaceImage1 = createSurfaceImage()
--Because the AnchorPoint of surfaceImage is (0.5, 0.5), its intended position is equal to the midpoint of the relevant hypotenuse.
surfaceImage1.Position = UDim2.new(0, (shortVertex1.X + shortVertex2.X) / 2, 0, (shortVertex1.Y + shortVertex2.Y) / 2)
--"SizeH" variables determine the length of the leg of the triangle represented by the surfaceImage that is perpendicular to the altitude line
local shortSizeH = UDim.new(0, math.sqrt(shortHypotenuseLength^2 - altitude^2))
--"SizeA" variables are always equal to the length of the altitude line
local shortSizeA = UDim.new(0, altitude)
--Determine the rotation of the base in degrees
local parallelRotation = getRotation(baseVertex1, baseVertex2)
--Determine whether the surfaceImage should be rotated by -90 degrees relative to the base before being rendered
local isPerpendicular = nil
if parallelRotation > 315 or parallelRotation <= 45 then
isPerpendicular = surfaceImage1.Position.X.Offset < vertexOppositeBase.X
elseif parallelRotation > 45 and parallelRotation <= 135 then
isPerpendicular = surfaceImage1.Position.Y.Offset < vertexOppositeBase.Y
elseif parallelRotation > 135 and parallelRotation <= 225 then
isPerpendicular = surfaceImage1.Position.X.Offset > vertexOppositeBase.X
else
isPerpendicular = surfaceImage1.Position.Y.Offset > vertexOppositeBase.Y
end
--If the surfaceImage should be rotated by -90 degrees relative to the base, reverse its size on the X and Y axes
surfaceImage1.Size = if isPerpendicular then UDim2.new(shortSizeA, shortSizeH) else UDim2.new(shortSizeH, shortSizeA)
surfaceImage1.Rotation = if isPerpendicular then parallelRotation - 90 else parallelRotation
surfaceImage1.Parent = mainGui.Background
--The following five lines are identical to the corresponding lines for the first surfaceImage, except the following lines utilize variables associated with the "long hypotenuse" rather than the "short hypotenuse"
local longVertex1, longVertex2 = getVertices(longHypotenuseKey)
local surfaceImage2 = createSurfaceImage()
surfaceImage2.Position = UDim2.new(0, (longVertex1.X + longVertex2.X) / 2, 0, (longVertex1.Y + longVertex2.Y) / 2)
local longSizeH = UDim.new(0, math.sqrt(longHypotenuseLength^2 - altitude^2))
local longSizeA = UDim2.new(0, altitude)
--If the first surfaceImage was rotated by -90 degrees relative to the base, this surfaceImage should not be, and vice versa
surfaceImage2.Size = if isPerpendicular then UDim2.new(longSizeH, longSizeA) else UDim2.new(longSizeA, longSizeH)
surfaceImage2.Rotation = if isPerpendicular then parallelRotation else parallelRotation - 90
surfaceImage2.Parent = mainGui.Background
end
I have not found other posts on the Developer Forum addressing this type of issue, and although other renderers do exist, they appear to function differently from the one I am attempting to create.
I do not currently understand what I am doing incorrectly in the above code, so if anyone is willing to offer assistance, that would be greatly appreciated.