Introduction
This document describes the process of turning DOM elements into render objects and how those objects are rendered into display entities.
The rendering is part of the delphirender
module.
Note: The document uses a lot of pseudo code with syntax similar to TypeScript, but breaks it occasionally for simplicity or brevity.
Data Structure
At the heart of the renderer is the data structure which more or less maps render objects to their DOM elements.
This uses a generic base class called RenderObject
which does
very little.
|
|
Aside from some utility functions and methods, that is all the class declares. Almost all of the heavy lifting is up to its inheritors. This class is extended by another render object class, whose purpose is to make handling render objects that only use 1 display entity easier:
|
|
The SingleEntityRenderObject
is extended by several other classes:
BoxRenderObject
This is a simple render object that is used to render a single rectangle.
When spawned, this render object sets the text display entity's content to be a single
0
character with text opacity24
. Because any text opacity under 26 stops the text from even being rendered... for some reason.TextRenderObject
- These are themselves actually an abstract class, divided into
StringRenderObject
andComponentRenderObject
, but they behave almost identically. ItemRenderObject
- Responsible for rendering itemstacks.
And lastly, there is the ElementRenderObject
:
|
|
They store the background (padding), border and outline as
BoxRenderObject
s in their boxes array, along with a list of
child elements. Element render objects also store a
ComputedStyleSet
which is the exact same style set as stored in
the CSS object model. Meaning that any changes to the CSS object model are
easily accessible in the render object. And the style: FullStyle
contains the fully computed style values.
The boxes
array in element render objects is initialized to
always be length 3, with each box being set to particular a part of the
element:
- Background (padding)
- Border
- Padding
Each of these boxes also has their depth calculated by iterating over each
box and multiplying its index by MICRO_LAYER_DEPTH
and adding
the element render object's depth
to it.
DOM to Render Tree Mapping
This section describes how the DOM is mapped to the render tree. Since this mapping process is handled differently per DOM Element and Node, I'll describe the conversion cases.
- Text nodes
- Text nodes are easily mapped by simply creating a
TextRenderObject
instance with the text node's content as the render object's content. <chat-component>
elements- An element render object is created, with it's
comp
field linked to the DOM element's style set. Then a single text render object is created (Same as in a text node's case) and set as the only child element of the element render object. <item>
elements- A
ItemRenderObject
is created with the item element's item stack as its content. Then we create an element render object, and set the item render object as its only child. <input>
elements- A
StringRenderObject
is created with the input element's placeholder or inputted value, depending if the value is unset. Then we create an element render object and set the string render object as its only child. - Elements
- An element render object is created. Then we iterate over all children of the DOM element and perform this same mapping on them, with the resulting render objects being added as children to the first element render object.
All render objects have their depth
field calculated by taking
DOM node's depth (Which is a flat integer) and multiplying it with
MACRO_LAYER_DEPTH
.
Note: For <item>
and
<chat-component>
render objects, the depths of the item
render object and text render object inside the resulting element render
object have to be manually set when mapping from the DOM.
Constants
This section will define some constants and try to quickly describe them.
CHAR_PX_SIZE_X
andCHAR_PX_SIZE_Y
- The size of a single pixel on an unscaled text display. Both constants
are equal to
0.025
CH_0_SIZE_X
- The width (in pixels) of a text display with the content
"0"
. This constant includes both the character pixels and the 1 pixel of padding the display entity has on both the left and right sides.The value of this constant is
7.0
CH_0_SIZE_Y
- The height (in pixels) of a text display with the content
"0"
. This constant includes both the character pixels and the 1 pixel of padding the display entity has on both the top and bottom.The value of this constant is
10.0
EMPTY_TD_BLOCK_SIZE_X
- The X scale of a text display with the content
"0"
stretched to fill a whole block.The value of this constant is
1.0f / (CHAR_PX_SIZE_X * CH_0_SIZE_X)
or around5.714286
EMPTY_TD_BLOCK_SIZE_Y
- The Y scale of a text display with the content
"0"
stretched to fill a whole block.The value of this constant is
1.0f / (CHAR_PX_SIZE_Y * CH_0_SIZE_Y)
or4.0
BLOCK_OFFSET_X
- The distance between a text display entity's visual center and its entity
origin point when it's been scaled to
EMPTY_TD_BLOCK_SIZE_X
andEMPTY_TD_BLOCK_SIZE_Y
.The value of this constant is
0.0717
I won't lie, this is a bad name for this constant.
MICRO_LAYER_DEPTH
- The distance between each box inside an element render object on the Z axis.
The value of this constant is
0.0001
Note: This is the smallest I could make this value before Z fighting became a major issue. Luckily, this value is also small enough so it doesn't look like elements are floating above other elements too obviously.
MACRO_LAYER_DEPTH
- The distance between each render object on the Z axis.
The value of this constant is
MICRO_LAYER_DEPTH * 3
, or0.0003
.
Render Screen
The render screen is a plane defined by a position, normal and 4 corners. It is the plane onto which render objects are drawn.
Render screens, although defined by the aforementioned properties, also contain more information. For one, render screens can be rotated to face any direction and be in any orientation possible. Screens can also be scaled.
To accomplish this, they also maintain a left rotation, scale and right rotation values.
It's required to note that the initial size of the screen is not changed when
the screen is transformed, rather a scalar is applied to it. This gives us
another property that is needed for projection later:
screenDimensionScale
. This property is the difference between the
set screen size and the actual size of the screen in the world.
Rendering Transformations
To render an element, the resulting display entity needs to be transformed in a variety of ways to align it correctly to where it is intended to be, and to ensure is aligned to the screen.
Firstly, I'm going to clarify for anyone that isn't aware, Minecraft provides
us with 4 transformations that are stored in a
Transformation
object. These 4 transformations are, in order:
- Translation
- This moves, offsets, the entity by a specified amount
- Left Rotation
- Rotates the entity. Called left rotation because it is applied before scaling, the next step
- Scaling
- Multiplies the entity's size.
- Right Rotation
- Rotates the entity. Called right rotation because it is applied after scaling.
Translation
Render objects consider their origin point to be the top left corner of the
object's bounding box, display entities, however, are aligned differently.
Text Displays are aligned to the bottom middle
As such, to align the entity correctly, we first offset the entity along the X axis by half the render object's size. Then, depending on if the entity is an item display or text display, we offset by either half its height, or by its full height, respectively on the Y axis.
And the Z component of the translation becomes equal to the render object's
depth value, plus the z-index
value, multiplied by
2.9999999e-4
.
Text and Box render objects also have to compensate for the visual alignment
offset bug display entities have.
|
|
The value returned from this function is then subtracted from X translation of the text display.
Scaling
Scaling is handled in a complicated to describe way. As such, I'll describe how its performed per each object type:
ItemRenderObject
Since we know that an item's sprite is, by default, the size of a block in GUI render mode, we can easily figure out the transformation scale by just dividing the intended size by that constant size.
As a side note, item displays are scaled down on the Z axis to prevent them from popping out of the page or from clipping into it. The Z scale is a constant of
0.075
. Values less than this tend to mess with the entity's brightness in unexpected ways.TextRenderObject
First, the object's base text size is measured. Then the intended size of the element is divided by the measured text's size to get the X and Y scale. Z scale remains unchanged.
BoxRenderObject
- The scale is gotten by multiplying the object's size x and y sizes with the
EMPTY_TD_BLOCK_SIZE_X
andEMPTY_TD_BLOCK_SIZE_Y
constants respectively.
Projection
Projection is what I choose to call the part of transformation where the display entity is rotated and scaled to be aligned to the screen.
First the X and Y components of the display translation are multiplied by the
screenDimensionScale
. Then the display scale is multiplied by
screen's scale. Then the screen's left and right rotations are applied onto
the display translation. And finally, the display's left and right rotations
are multiplied by the screen's left and right rotations respectively.
Notes on Transformations
ItemRenderObject
s perform the translation and scaling step
differently. They execute the same operations, however, they do not use their
own render object's size to perform these calculations, they use the parent
size.
This is because otherwise, the item display wouldn't scale as intended with
CSS changes to the parent ElementRenderObject
.
Notes
- Not actually the middle. They are slightly offset from the middle, this has caused me so much headache. See Dev Journal, 05-02-2025