changeset 443:7629f85823a4

fix: font-api, allow consistent use of mesh-transform
author Sam <sam@basx.dev>
date Sat, 24 Feb 2024 14:31:15 +0700
parents 637974701484
children f9d25bc331b3
files semicongine/core/constants.nim semicongine/core/dynamic_arrays.nim semicongine/engine.nim semicongine/renderer.nim semicongine/scene.nim semicongine/text.nim tests/test_font.nim
diffstat 7 files changed, 131 insertions(+), 158 deletions(-) [+]
line wrap: on
line diff
--- a/semicongine/core/constants.nim	Sat Feb 17 17:18:35 2024 +0700
+++ b/semicongine/core/constants.nim	Sat Feb 24 14:31:15 2024 +0700
@@ -4,3 +4,4 @@
   ENGINEVERSION* = "0.0.1"
   TRANSFORM_ATTRIB* = "transform"
   MATERIALINDEX_ATTRIBUTE* = "materialIndex"
+  ASPECT_RATIO_ATTRIBUTE* = "aspect_ratio"
--- a/semicongine/core/dynamic_arrays.nim	Sat Feb 17 17:18:35 2024 +0700
+++ b/semicongine/core/dynamic_arrays.nim	Sat Feb 24 14:31:15 2024 +0700
@@ -201,57 +201,57 @@
     of TextureType: discard
 
 
-proc setValues[T: GPUType|int|uint|float](value: var DataList, data: seq[T]) =
+proc setValues[T: GPUType|int|uint|float](value: var DataList, data: openArray[T]) =
   value.setLen(data.len)
-  when T is float32: value.float32[] = data
-  elif T is float64: value.float64[] = data
-  elif T is int8: value.int8[] = data
-  elif T is int16: value.int16[] = data
-  elif T is int32: value.int32[] = data
-  elif T is int64: value.int64[] = data
-  elif T is uint8: value.uint8[] = data
-  elif T is uint16: value.uint16[] = data
-  elif T is uint32: value.uint32[] = data
-  elif T is uint64: value.uint64[] = data
-  elif T is int and sizeof(int) == sizeof(int32): value.int32[] = data
-  elif T is int and sizeof(int) == sizeof(int64): value.int64[] = data
-  elif T is uint and sizeof(uint) == sizeof(uint32): value.uint32[] = data
-  elif T is uint and sizeof(uint) == sizeof(uint64): value.uint64[] = data
-  elif T is float and sizeof(float) == sizeof(float32): value.float32[] = data
-  elif T is float and sizeof(float) == sizeof(float64): value.float64[] = data
-  elif T is TVec2[int32]: value.vec2i32[] = data
-  elif T is TVec2[int64]: value.vec2i64[] = data
-  elif T is TVec3[int32]: value.vec3i32[] = data
-  elif T is TVec3[int64]: value.vec3i64[] = data
-  elif T is TVec4[int32]: value.vec4i32[] = data
-  elif T is TVec4[int64]: value.vec4i64[] = data
-  elif T is TVec2[uint32]: value.vec2u32[] = data
-  elif T is TVec2[uint64]: value.vec2u64[] = data
-  elif T is TVec3[uint32]: value.vec3u32[] = data
-  elif T is TVec3[uint64]: value.vec3u64[] = data
-  elif T is TVec4[uint32]: value.vec4u32[] = data
-  elif T is TVec4[uint64]: value.vec4u64[] = data
-  elif T is TVec2[float32]: value.vec2f32[] = data
-  elif T is TVec2[float64]: value.vec2f64[] = data
-  elif T is TVec3[float32]: value.vec3f32[] = data
-  elif T is TVec3[float64]: value.vec3f64[] = data
-  elif T is TVec4[float32]: value.vec4f32[] = data
-  elif T is TVec4[float64]: value.vec4f64[] = data
-  elif T is TMat2[float32]: value.mat2f32[] = data
-  elif T is TMat2[float64]: value.mat2f64[] = data
-  elif T is TMat23[float32]: value.mat23f32[] = data
-  elif T is TMat23[float64]: value.mat23f64[] = data
-  elif T is TMat32[float32]: value.mat32f32[] = data
-  elif T is TMat32[float64]: value.mat32f64[] = data
-  elif T is TMat3[float32]: value.mat3f32[] = data
-  elif T is TMat3[float64]: value.mat3f64[] = data
-  elif T is TMat34[float32]: value.mat34f32[] = data
-  elif T is TMat34[float64]: value.mat34f64[] = data
-  elif T is TMat43[float32]: value.mat43f32[] = data
-  elif T is TMat43[float64]: value.mat43f64[] = data
-  elif T is TMat4[float32]: value.mat4f32[] = data
-  elif T is TMat4[float64]: value.mat4f64[] = data
-  elif T is Texture: value.texture[] = data
+  when T is float32: value.float32[] = @data
+  elif T is float64: value.float64[] = @data
+  elif T is int8: value.int8[] = @data
+  elif T is int16: value.int16[] = @data
+  elif T is int32: value.int32[] = @data
+  elif T is int64: value.int64[] = @data
+  elif T is uint8: value.uint8[] = @data
+  elif T is uint16: value.uint16[] = @data
+  elif T is uint32: value.uint32[] = @data
+  elif T is uint64: value.uint64[] = @data
+  elif T is int and sizeof(int) == sizeof(int32): value.int32[] = @data
+  elif T is int and sizeof(int) == sizeof(int64): value.int64[] = @data
+  elif T is uint and sizeof(uint) == sizeof(uint32): value.uint32[] = @data
+  elif T is uint and sizeof(uint) == sizeof(uint64): value.uint64[] = @data
+  elif T is float and sizeof(float) == sizeof(float32): value.float32[] = @data
+  elif T is float and sizeof(float) == sizeof(float64): value.float64[] = @data
+  elif T is TVec2[int32]: value.vec2i32[] = @data
+  elif T is TVec2[int64]: value.vec2i64[] = @data
+  elif T is TVec3[int32]: value.vec3i32[] = @data
+  elif T is TVec3[int64]: value.vec3i64[] = @data
+  elif T is TVec4[int32]: value.vec4i32[] = @data
+  elif T is TVec4[int64]: value.vec4i64[] = @data
+  elif T is TVec2[uint32]: value.vec2u32[] = @data
+  elif T is TVec2[uint64]: value.vec2u64[] = @data
+  elif T is TVec3[uint32]: value.vec3u32[] = @data
+  elif T is TVec3[uint64]: value.vec3u64[] = @data
+  elif T is TVec4[uint32]: value.vec4u32[] = @data
+  elif T is TVec4[uint64]: value.vec4u64[] = @data
+  elif T is TVec2[float32]: value.vec2f32[] = @data
+  elif T is TVec2[float64]: value.vec2f64[] = @data
+  elif T is TVec3[float32]: value.vec3f32[] = @data
+  elif T is TVec3[float64]: value.vec3f64[] = @data
+  elif T is TVec4[float32]: value.vec4f32[] = @data
+  elif T is TVec4[float64]: value.vec4f64[] = @data
+  elif T is TMat2[float32]: value.mat2f32[] = @data
+  elif T is TMat2[float64]: value.mat2f64[] = @data
+  elif T is TMat23[float32]: value.mat23f32[] = @data
+  elif T is TMat23[float64]: value.mat23f64[] = @data
+  elif T is TMat32[float32]: value.mat32f32[] = @data
+  elif T is TMat32[float64]: value.mat32f64[] = @data
+  elif T is TMat3[float32]: value.mat3f32[] = @data
+  elif T is TMat3[float64]: value.mat3f64[] = @data
+  elif T is TMat34[float32]: value.mat34f32[] = @data
+  elif T is TMat34[float64]: value.mat34f64[] = @data
+  elif T is TMat43[float32]: value.mat43f32[] = @data
+  elif T is TMat43[float64]: value.mat43f64[] = @data
+  elif T is TMat4[float32]: value.mat4f32[] = @data
+  elif T is TMat4[float64]: value.mat4f64[] = @data
+  elif T is Texture: value.texture[] = @data
   else: {.error: "Virtual datatype has no values".}
 
 proc setValue[T: GPUType|int|uint|float](value: var DataList, i: int, data: T) =
@@ -473,13 +473,13 @@
   getValue[t](list, i)
 
 # since we use this often with tables, add this for an easy assignment
-template `[]=`*[T](table: var Table[string, DataList], key: string, values: seq[T]) =
-  if key in table:
+template `[]=`*[T](table: var Table[string, DataList], key: string, values: openArray[T]) =
+  if table.contains(key):
     table[key].setValues(values)
   else:
     table[key] = initDataList(values)
 
-template `[]=`*[T](list: var DataList, values: seq[T]) =
+template `[]=`*[T](list: var DataList, values: openArray[T]) =
   list.setValues(values)
 template `[]=`*[T](list: var DataList, i: int, value: T) =
   list.setValue(i, value)
@@ -532,57 +532,57 @@
     of Mat4F64: result = value.mat4f64[].toCPointer
     of TextureType: nil
 
-proc appendValues*[T: GPUType|int|uint|float](value: var DataList, data: seq[T]) =
+proc appendValues*[T: GPUType|int|uint|float](value: var DataList, data: openArray[T]) =
   value.len += data.len
-  when T is float32: value.float32[].add data
-  elif T is float64: value.float64[].add data
-  elif T is int8: value.int8[].add data
-  elif T is int16: value.int16[].add data
-  elif T is int32: value.int32[].add data
-  elif T is int64: value.int64[].add data
-  elif T is uint8: value.uint8[].add data
-  elif T is uint16: value.uint16[].add data
-  elif T is uint32: value.uint32[].add data
-  elif T is uint64: value.uint64[].add data
-  elif T is int and sizeof(int) == sizeof(int32): value.int32[].add data
-  elif T is int and sizeof(int) == sizeof(int64): value.int64[].add data
-  elif T is uint and sizeof(uint) == sizeof(uint32): value.uint32[].add data
-  elif T is uint and sizeof(uint) == sizeof(uint64): value.uint64[].add data
-  elif T is float and sizeof(float) == sizeof(float32): value.float32[].add data
-  elif T is float and sizeof(float) == sizeof(float64): value.float64[].add data
-  elif T is TVec2[int32]: value.vec2i32[].add data
-  elif T is TVec2[int64]: value.vec2i64[].add data
-  elif T is TVec3[int32]: value.vec3i32[].add data
-  elif T is TVec3[int64]: value.vec3i64[].add data
-  elif T is TVec4[int32]: value.vec4i32[].add data
-  elif T is TVec4[int64]: value.vec4i64[].add data
-  elif T is TVec2[uint32]: value.vec2u32[].add data
-  elif T is TVec2[uint64]: value.vec2u64[].add data
-  elif T is TVec3[uint32]: value.vec3u32[].add data
-  elif T is TVec3[uint64]: value.vec3u64[].add data
-  elif T is TVec4[uint32]: value.vec4u32[].add data
-  elif T is TVec4[uint64]: value.vec4u64[].add data
-  elif T is TVec2[float32]: value.vec2f32[].add data
-  elif T is TVec2[float64]: value.vec2f64[].add data
-  elif T is TVec3[float32]: value.vec3f32[].add data
-  elif T is TVec3[float64]: value.vec3f64[].add data
-  elif T is TVec4[float32]: value.vec4f32[].add data
-  elif T is TVec4[float64]: value.vec4f64[].add data
-  elif T is TMat2[float32]: value.mat2f32[].add data
-  elif T is TMat2[float64]: value.mat2f64[].add data
-  elif T is TMat23[float32]: value.mat23f32[].add data
-  elif T is TMat23[float64]: value.mat23f64[].add data
-  elif T is TMat32[float32]: value.mat32f32[].add data
-  elif T is TMat32[float64]: value.mat32f64[].add data
-  elif T is TMat3[float32]: value.mat3f32[].add data
-  elif T is TMat3[float64]: value.mat3f64[].add data
-  elif T is TMat34[float32]: value.mat34f32[].add data
-  elif T is TMat34[float64]: value.mat34f64[].add data
-  elif T is TMat43[float32]: value.mat43f32[].add data
-  elif T is TMat43[float64]: value.mat43f64[].add data
-  elif T is TMat4[float32]: value.mat4f32[].add data
-  elif T is TMat4[float64]: value.mat4f64[].add data
-  elif T is Texture: value.texture[].add data
+  when T is float32: value.float32[].add @data
+  elif T is float64: value.float64[].add @data
+  elif T is int8: value.int8[].add @data
+  elif T is int16: value.int16[].add @data
+  elif T is int32: value.int32[].add @data
+  elif T is int64: value.int64[].add @data
+  elif T is uint8: value.uint8[].add @data
+  elif T is uint16: value.uint16[].add @data
+  elif T is uint32: value.uint32[].add @data
+  elif T is uint64: value.uint64[].add @data
+  elif T is int and sizeof(int) == sizeof(int32): value.int32[].add @data
+  elif T is int and sizeof(int) == sizeof(int64): value.int64[].add @data
+  elif T is uint and sizeof(uint) == sizeof(uint32): value.uint32[].add @data
+  elif T is uint and sizeof(uint) == sizeof(uint64): value.uint64[].add @data
+  elif T is float and sizeof(float) == sizeof(float32): value.float32[].add @data
+  elif T is float and sizeof(float) == sizeof(float64): value.float64[].add @data
+  elif T is TVec2[int32]: value.vec2i32[].add @data
+  elif T is TVec2[int64]: value.vec2i64[].add @data
+  elif T is TVec3[int32]: value.vec3i32[].add @data
+  elif T is TVec3[int64]: value.vec3i64[].add @data
+  elif T is TVec4[int32]: value.vec4i32[].add @data
+  elif T is TVec4[int64]: value.vec4i64[].add @data
+  elif T is TVec2[uint32]: value.vec2u32[].add @data
+  elif T is TVec2[uint64]: value.vec2u64[].add @data
+  elif T is TVec3[uint32]: value.vec3u32[].add @data
+  elif T is TVec3[uint64]: value.vec3u64[].add @data
+  elif T is TVec4[uint32]: value.vec4u32[].add @data
+  elif T is TVec4[uint64]: value.vec4u64[].add @data
+  elif T is TVec2[float32]: value.vec2f32[].add @data
+  elif T is TVec2[float64]: value.vec2f64[].add @data
+  elif T is TVec3[float32]: value.vec3f32[].add @data
+  elif T is TVec3[float64]: value.vec3f64[].add @data
+  elif T is TVec4[float32]: value.vec4f32[].add @data
+  elif T is TVec4[float64]: value.vec4f64[].add @data
+  elif T is TMat2[float32]: value.mat2f32[].add @data
+  elif T is TMat2[float64]: value.mat2f64[].add @data
+  elif T is TMat23[float32]: value.mat23f32[].add @data
+  elif T is TMat23[float64]: value.mat23f64[].add @data
+  elif T is TMat32[float32]: value.mat32f32[].add @data
+  elif T is TMat32[float64]: value.mat32f64[].add @data
+  elif T is TMat3[float32]: value.mat3f32[].add @data
+  elif T is TMat3[float64]: value.mat3f64[].add @data
+  elif T is TMat34[float32]: value.mat34f32[].add @data
+  elif T is TMat34[float64]: value.mat34f64[].add @data
+  elif T is TMat43[float32]: value.mat43f32[].add @data
+  elif T is TMat43[float64]: value.mat43f64[].add @data
+  elif T is TMat4[float32]: value.mat4f32[].add @data
+  elif T is TMat4[float64]: value.mat4f64[].add @data
+  elif T is Texture: value.texture[].add @data
   else: {.error: "Virtual datatype has no values".}
 
 proc appendValues*(value: var DataList, data: DataList) =
--- a/semicongine/engine.nim	Sat Feb 17 17:18:35 2024 +0700
+++ b/semicongine/engine.nim	Sat Feb 24 14:31:15 2024 +0700
@@ -1,3 +1,5 @@
+{.experimental: "codeReordering".}
+
 import std/options
 import std/logging
 import std/os
@@ -120,6 +122,7 @@
 proc loadScene*(engine: var Engine, scene: var Scene) =
   assert engine.renderer.isSome
   assert not scene.loaded
+  scene.addShaderGlobal(ASPECT_RATIO_ATTRIBUTE, engine.getAspectRatio)
   engine.renderer.get.setupDrawableBuffers(scene)
   engine.renderer.get.updateMeshData(scene, forceAll = true)
   engine.renderer.get.updateUniformData(scene, forceAll = true)
@@ -130,6 +133,7 @@
 proc renderScene*(engine: var Engine, scene: var Scene) =
   assert engine.state == Running
   assert engine.renderer.isSome
+  scene.setShaderGlobal(ASPECT_RATIO_ATTRIBUTE, engine.getAspectRatio)
   engine.renderer.get.updateMeshData(scene)
   engine.renderer.get.updateUniformData(scene)
   engine.renderer.get.render(scene)
@@ -208,7 +212,7 @@
 func framesRendered*(engine: Engine): uint64 = (if engine.renderer.isSome: engine.renderer.get.framesRendered else: 0)
 func gpuDevice*(engine: Engine): Device = engine.device
 func getWindow*(engine: Engine): auto = engine.window
-func getAspectRatio*(engine: Engine): auto = engine.getWindow().size[0] / engine.getWindow().size[1]
+func getAspectRatio*(engine: Engine): float32 = engine.getWindow().size[0] / engine.getWindow().size[1]
 func windowWasResized*(engine: Engine): auto = engine.input.windowWasResized
 func showSystemCursor*(engine: Engine) = engine.window.showSystemCursor()
 func hideSystemCursor*(engine: Engine) = engine.window.hideSystemCursor()
@@ -221,14 +225,7 @@
 func limits*(engine: Engine): VkPhysicalDeviceLimits =
   engine.gpuDevice().physicalDevice.properties.limits
 
-proc processEventsFor*(engine: Engine, text: var Text) =
-  if engine.input.windowWasResized:
-    text.aspect_ratio = engine.getAspectRatio()
-  text.refresh()
-
 proc processEventsFor*(engine: Engine, panel: var Panel) =
-  if engine.input.windowWasResized:
-    panel.aspect_ratio = engine.getAspectRatio()
   panel.refresh()
 
   let hasMouseNow = panel.contains(engine.mousePositionNormalized())
--- a/semicongine/renderer.nim	Sat Feb 17 17:18:35 2024 +0700
+++ b/semicongine/renderer.nim	Sat Feb 24 14:31:15 2024 +0700
@@ -72,7 +72,7 @@
             result.add input
             found[input.name] = input
 
-func materialCompatibleWithPipeline(scene: Scene, materialType: MaterialType, shaderPipeline: ShaderPipeline): (bool, string) =
+proc materialCompatibleWithPipeline(scene: Scene, materialType: MaterialType, shaderPipeline: ShaderPipeline): (bool, string) =
   for uniform in shaderPipeline.uniforms:
     if scene.shaderGlobals.contains(uniform.name):
       if scene.shaderGlobals[uniform.name].theType != uniform.theType:
@@ -90,7 +90,7 @@
 
   return (false, "")
 
-func meshCompatibleWithPipeline(scene: Scene, mesh: Mesh, shaderPipeline: ShaderPipeline): (bool, string) =
+proc meshCompatibleWithPipeline(scene: Scene, mesh: Mesh, shaderPipeline: ShaderPipeline): (bool, string) =
   for input in shaderPipeline.inputs:
     if input.name in [TRANSFORM_ATTRIB, MATERIALINDEX_ATTRIBUTE]: # will be populated automatically
       assert input.perInstance == true, &"Currently the {input.name} attribute must be a per instance attribute"
@@ -109,7 +109,7 @@
     return (true, pipelineCompatability[1])
   return (false, "")
 
-func checkSceneIntegrity(renderer: Renderer, scene: Scene) =
+proc checkSceneIntegrity(renderer: Renderer, scene: Scene) =
   # TODO: this and the sub-functions can likely be simplified a ton
   if scene.meshes.len == 0:
     return
--- a/semicongine/scene.nim	Sat Feb 17 17:18:35 2024 +0700
+++ b/semicongine/scene.nim	Sat Feb 24 14:31:15 2024 +0700
@@ -8,7 +8,7 @@
 import ./material
 
 type
-  Scene* = object
+  Scene* = ref object
     name*: string
     shaderGlobals*: Table[string, DataList]
     meshes*: seq[Mesh]
@@ -43,37 +43,32 @@
       assert not value.isNil, &"Cannot add a mesh that is 'nil': " & name
       scene.meshes.add value
 
-proc addShaderGlobal*[T](scene: var Scene, name: string, data: T) =
-  assert not scene.loaded, &"Scene {scene.name} has already been loaded, cannot add shader values"
-  scene.shaderGlobals[name] = initDataList(thetype = getDataType[T]())
-  scene.shaderGlobals[name] = @[data]
-  scene.dirtyShaderGlobals.add name
-
 proc addShaderGlobalArray*[T](scene: var Scene, name: string, data: openArray[T]) =
   assert not scene.loaded, &"Scene {scene.name} has already been loaded, cannot add shader values"
   scene.shaderGlobals[name] = initDataList(data)
   scene.dirtyShaderGlobals.add name
 
-func getShaderGlobal*[T](scene: Scene, name: string): T =
-  scene.shaderGlobals[name][T, 0]
+proc addShaderGlobal*[T](scene: var Scene, name: string, data: T) =
+  scene.addShaderGlobalArray(name, [data])
 
 func getShaderGlobalArray*[T](scene: Scene, name: string): ref seq[T] =
   scene.shaderGlobals[name][T]
 
-proc setShaderGlobal*[T](scene: var Scene, name: string, value: T) =
-  scene.shaderGlobals[name] = @[value]
-  if not scene.dirtyShaderGlobals.contains(name):
-    scene.dirtyShaderGlobals.add name
+func getShaderGlobal*[T](scene: Scene, name: string): T =
+  scene.getShaderGlobalArray(name)[][0]
 
-proc setShaderGlobalArray*[T](scene: var Scene, name: string, value: seq[T]) =
+proc setShaderGlobalArray*[T](scene: var Scene, name: string, value: openArray[T]) =
   scene.shaderGlobals[name] = value
   if not scene.dirtyShaderGlobals.contains(name):
     scene.dirtyShaderGlobals.add name
 
+proc setShaderGlobal*[T](scene: var Scene, name: string, value: T) =
+  scene.setShaderGlobalArray(name, [value])
+
 func dirtyShaderGlobals*(scene: Scene): seq[string] =
   scene.dirtyShaderGlobals
 
-func clearDirtyShaderGlobals*(scene: var Scene) =
+proc clearDirtyShaderGlobals*(scene: var Scene) =
   scene.dirtyShaderGlobals.reset
 
 func hash*(scene: Scene): Hash =
--- a/semicongine/text.nim	Sat Feb 17 17:18:35 2024 +0700
+++ b/semicongine/text.nim	Sat Feb 24 14:31:15 2024 +0700
@@ -34,10 +34,10 @@
       attr[uint16]("materialIndexOut", noInterpolation = true)
     ],
     outputs = [attr[Vec4f]("color")],
-    uniforms = [attr[Vec4f]("color", arrayCount = MAX_TEXT_MATERIALS)],
+    uniforms = [attr[Vec4f]("color", arrayCount = MAX_TEXT_MATERIALS), attr[float32](ASPECT_RATIO_ATTRIBUTE)],
     samplers = [attr[Texture]("fontAtlas", arrayCount = MAX_TEXT_MATERIALS)],
     vertexCode = &"""
-  gl_Position = vec4({POSITION_ATTRIB}, 1.0) * {TRANSFORM_ATTRIB};
+  gl_Position = vec4({POSITION_ATTRIB}.x, {POSITION_ATTRIB}.y * Uniforms.{ASPECT_RATIO_ATTRIBUTE}, {POSITION_ATTRIB}.z, 1.0) * {TRANSFORM_ATTRIB};
   uvFrag = {UV_ATTRIB};
   materialIndexOut = {MATERIALINDEX_ATTRIBUTE};
   """,
@@ -53,11 +53,8 @@
     maxWidth: float32 = 0
     # properties:
     text: seq[Rune]
-    position: Vec2f
     horizontalAlignment: HorizontalAlignment = Center
     verticalAlignment: VerticalAlignment = Center
-    scale: float32
-    aspect_ratio: float32
     # management/internal:
     dirty: bool                 # is true if any of the attributes changed
     processedText: seq[Rune]    # used to store processed (word-wrapper) text to preserve original
@@ -142,7 +139,6 @@
       text.mesh[POSITION_ATTRIB, vertexOffset + 1] = newVec3f()
       text.mesh[POSITION_ATTRIB, vertexOffset + 2] = newVec3f()
       text.mesh[POSITION_ATTRIB, vertexOffset + 3] = newVec3f()
-  text.mesh.transform = translate(text.position.x, text.position.y * text.aspect_ratio, 0) * scale(text.scale, text.scale * text.aspect_ratio)
   text.lastRenderedText = text.processedText
   text.dirty = false
 
@@ -215,7 +211,7 @@
 
   text.processedText = text.text
   if text.maxWidth > 0:
-    text.processedText = text.processedText.wordWrapped(text.font, text.maxWidth / text.scale)
+    text.processedText = text.processedText.wordWrapped(text.font, text.maxWidth / text.mesh.transform.scaling.x)
 
 proc `text=`*(text: var Text, newText: string) =
   `text=`(text, newText.toRunes)
@@ -247,21 +243,7 @@
     text.verticalAlignment = value
     text.dirty = true
 
-proc scale*(text: Text): float32 =
-  text.scale
-proc `scale=`*(text: var Text, value: float32) =
-  if value != text.scale:
-    text.scale = value
-    text.dirty = true
-
-proc aspect_ratio*(text: Text): float32 =
-  text.aspect_ratio
-proc `aspect_ratio=`*(text: var Text, value: float32) =
-  if value != text.aspect_ratio:
-    text.aspect_ratio = value
-    text.dirty = true
-
-proc initText*(font: Font, text = "".toRunes, maxLen: int = text.len, color = newVec4f(0.07, 0.07, 0.07, 1), scale = 1'f32, position = newVec2f(), verticalAlignment = VerticalAlignment.Center, horizontalAlignment = HorizontalAlignment.Center, maxWidth = 0'f32): Text =
+proc initText*(font: Font, text = "".toRunes, maxLen: int = text.len, color = newVec4f(0.07, 0.07, 0.07, 1), position = newVec2f(), verticalAlignment = VerticalAlignment.Center, horizontalAlignment = HorizontalAlignment.Center, maxWidth = 0'f32, transform = Unit4): Text =
   var
     positions = newSeq[Vec3f](int(maxLen * 4))
     indices: seq[array[3, uint16]]
@@ -273,8 +255,7 @@
       [uint16(offset + 2), uint16(offset + 3), uint16(offset + 0)],
     ]
 
-  result = Text(maxLen: maxLen, font: font, dirty: true, scale: scale, position: position, aspect_ratio: 1, horizontalAlignment: horizontalAlignment, verticalAlignment: verticalAlignment, maxWidth: maxWidth)
-  `text=`(result, text)
+  result = Text(maxLen: maxLen, font: font, dirty: true, horizontalAlignment: horizontalAlignment, verticalAlignment: verticalAlignment, maxWidth: maxWidth)
   result.mesh = newMesh(positions = positions, indices = indices, uvs = uvs, name = &"text-{instanceCounter}")
   result.mesh[].renameAttribute("position", POSITION_ATTRIB)
   result.mesh[].renameAttribute("uv", UV_ATTRIB)
@@ -283,6 +264,8 @@
     name = font.name & " text",
     attributes = {"fontAtlas": initDataList(@[font.fontAtlas]), "color": initDataList(@[color])},
   )
+  result.mesh.transform = transform
+  `text=`(result, text)
   inc instanceCounter
 
   result.refresh()
--- a/tests/test_font.nim	Sat Feb 17 17:18:35 2024 +0700
+++ b/tests/test_font.nim	Sat Feb 24 14:31:15 2024 +0700
@@ -13,7 +13,7 @@
   var scene = Scene(name: "main")
   var font = loadFont("DejaVuSans.ttf", lineHeightPixels = 210'f32)
   var origin = initPanel(size = newVec2f(0.01, 0.01))
-  var main_text = font.initText("", maxLen = 255, color = newVec4f(1, 0.15, 0.15, 1), scale = 0.0005, maxWidth = 1.0)
+  var main_text = font.initText("".toRunes, maxLen = 255, color = newVec4f(1, 0.15, 0.15, 1), maxWidth = 1.0, transform = scale(0.0005, 0.0005))
   var help_text = font.initText("""Controls
 
 Horizontal alignment:
@@ -23,7 +23,7 @@
 Vertical alignment:
   F4: Top
   F5: Center
-  F6: Bottom""", scale = 0.0002, position = newVec2f(-0.9, -0.9), horizontalAlignment = Left, verticalAlignment = Top)
+  F6: Bottom""".toRunes, horizontalAlignment = Left, verticalAlignment = Top, transform = translate(-0.9, -0.9) * scale(0.0002, 0.0002))
   scene.add origin
   scene.add main_text
   scene.add help_text
@@ -36,9 +36,6 @@
     main_text.color = newVec4f(sin(t) * 0.5 + 0.5, 0.15, 0.15, 1)
     if engine.windowWasResized():
       var winSize = engine.getWindow().size
-      main_text.aspect_ratio = winSize[0] / winSize[1]
-      origin.aspect_ratio = winSize[0] / winSize[1]
-      help_text.aspect_ratio = winSize[0] / winSize[1]
 
     # add character
     if main_text.text.len < main_text.maxLen - 1: