diff --git a/game/game.py b/game/game.py
index ad30abe..7abc63f 100644
--- a/game/game.py
+++ b/game/game.py
@@ -13,24 +13,26 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-import time
from math import pi, tau, dist
-from ctypes import Structure
from engine import *
from game import math
-from game import triangles
-from game import sea
-from game.shader import Shader
-from game.generator import Generator
-from game.resources import RuntimeArchive
-from game.batch import Batch
-from game.camera import Camera
-from game.environment import Environment
+from game.time import Time
from game.events import Events
from game.mouse import Mouse
from game.keyboard import Keyboard
+from game.generator import Generator
+from game.resources import RuntimeArchive
+from game.texture import Texture
+from game.shader import Shader
+from game.camera import Camera
+from game.environment import Environment
+from game.inputs import InputFloat
+from game.batch import Batch
+from game.scene import SceneNode, TextureNode, ShaderNode, InputNode, DrawNode, FuncNode
+from game.triangles import SkyTriangles
+from game import sea
proj_hfov = pi * 0.25
proj_ratio = 16.0 / 9.0
@@ -40,27 +42,49 @@ proj_far_z = 3000.0
sun_direction = math.vec3_normalize((1.0, 0.0, 0.5))
sun_power = 1.0
-def main():
- print("Generating terrain...")
- gen_begin = time.thread_time()
- generated = Generator(256)
- gen_end = time.thread_time()
- print("Done: ", round(gen_end - gen_begin, 2), "seconds")
+def update_camera(time, mouse, camera, environment):
+ camera_yaw = mouse.drag[0] * 0.001
+ camera_pitch = mouse.drag[1] * 0.001 + pi * 0.25
+ camera_distance = mouse.wheel * 20.0
+ camera.set_view(camera_yaw, camera_pitch, camera_distance)
+ environment.from_sun(camera.view, sun_direction, sun_power)
+def update_tests(time, blob_translation, blob_spawn_translation, cube_translation, cube_spawn_translation, cube_orientation, clouds_orientation):
+ rotation = mat3()
+ mat3_rotation(rotation, vec3_up, (time.current * 0.21) % tau)
+ mat3_mul_vec3(blob_translation, rotation, blob_spawn_translation)
+ mat3_mul_vec3(cube_translation, rotation, cube_spawn_translation)
+ mat3_rotation(cube_orientation, vec3_up, (time.current * 0.43) % tau)
+ mat3_rotation(clouds_orientation, vec3_up, (time.current * -0.037) % tau)
+
+def update_sea(time, camera, sea_phase):
+ camera.to_km()
+ sea_phase.update((time.current * 0.023) % 1.0)
+
+def main():
print("Initializing...")
display = create_display(b'RK Island - Drag to rotate, wheel to zoom, q to quit', 1600, 900)
events = Events(display)
mouse = Mouse(events, wheel = 60, wheel_min = 20)
keyboard = Keyboard(events)
render_initialize(__debug__)
- terrain_shader = Shader('terrain', 'common')
+
+ tiles_shader = Shader('terrain', 'common')
tests_shader = Shader('tests', 'common')
sky_shader = Shader('sky')
- heightmap = create_texture(
+ camera = Camera()
+ camera.set_projection(proj_hfov, proj_ratio, proj_near_z, proj_far_z)
+ environment = Environment()
+ sea_phase = InputFloat(sky_shader.u_sea_phase, 0.0)
+
+ print("Generating terrain...")
+ generated = Generator(256)
+
+ heightmap = Texture(
TEXTURE_FORMAT_FLOAT_32, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR,
generated.packed_heights)
- normalmap = create_texture(
+ normalmap = Texture(
TEXTURE_FORMAT_RGB10_A2, 256, 256, 0, TEXTURE_FLAG_MIN_LINEAR | TEXTURE_FLAG_MAG_LINEAR,
generated.packed_normals)
@@ -77,7 +101,7 @@ def main():
rock_model = archive.get_model('rock')
mud_model = archive.get_model('mud')
lava_model = archive.get_model('lava')
- terrain_batch = Batch(tiles_vertices, generated.size ** 2, 8, translation = PARAM_FORMAT_VEC3_SHORT)
+ tiles_batch = Batch(tiles_vertices, generated.size ** 2, 8, translation = PARAM_FORMAT_VEC3_SHORT)
#TODO: generator & for real
vc = generated.volcano_c
@@ -108,7 +132,7 @@ def main():
model = mud_model
else:
model = rock_model
- model.spawn(terrain_batch, vec3(float(((mx - 128) * 8) + 4), float(((127 - my) * 8) + 4), 0.0))
+ model.spawn(tiles_batch, vec3(float(((mx - 128) * 8) + 4), float(((127 - my) * 8) + 4), 0.0))
tests_texture = archive.get_texture('tests')
tests_vertices = archive.get_vertices('tests')
@@ -118,153 +142,75 @@ def main():
tests_batch = Batch(tests_vertices, 3, 3,
translation = PARAM_FORMAT_VEC3_FLOAT,
orientation = PARAM_FORMAT_MAT3_INT10 | PARAM_FORMAT_NORMALIZE)
+
blob_spawn_translation = vec3(-100.0, -500.0, 0.0)
- cube_spawn_translation = vec3(100.0, -500.0, 0.0)
blob_forward = math.vec3_normalize((sun_direction[0], sun_direction[1], 0.0))
blob_right = math.vec3_cross(blob_forward, math.vec3_up)
- blob_id = blob_model.spawn(tests_batch,
- blob_spawn_translation, mat3(vec3(*blob_right), vec3(*blob_forward), vec3_up))
+ blob_spawn_orientation = mat3(vec3(*blob_right), vec3(*blob_forward), vec3_up)
+ blob_id = blob_model.spawn(tests_batch, blob_spawn_translation, blob_spawn_orientation)
+ blob_translation = tests_batch.translation[blob_id]
+ cube_spawn_translation = vec3(100.0, -500.0, 0.0)
cube_id = cube_model.spawn(tests_batch, cube_spawn_translation, mat3_identity)
+ cube_translation = tests_batch.translation[cube_id]
+ cube_orientation = tests_batch.orientation[cube_id]
clouds_id = clouds_model.spawn(tests_batch, vec3(0.0, 0.0, 32.0), mat3_identity)
+ clouds_orientation = tests_batch.orientation[clouds_id]
sea_polar_textures = sea.load_polar_textures(('data/sea_bump1.png', 'data/sea_bump2.png'))
sea_detail_texture = sea.load_detail_texture('data/sea_bump.png')
- sky_triangles = create_triangles(triangles.sky_triangles(64, proj_far_z - 0.1, proj_ratio))
+ sky_triangles = SkyTriangles(64, proj_far_z - 0.1, proj_ratio)
- camera = Camera()
- camera.set_projection(proj_hfov, proj_ratio, proj_near_z, proj_far_z)
- environment = Environment()
-
- blob_translation = tests_batch.translation[blob_id]
- cube_translation = tests_batch.translation[cube_id]
- cube_orientation = tests_batch.orientation[cube_id]
- clouds_orientation = tests_batch.orientation[clouds_id]
+ scene = SceneNode(
+ FuncNode(update_camera, (mouse, camera, environment)),
+ TextureNode((tiles_texture, heightmap, normalmap),
+ ShaderNode(tiles_shader,
+ InputNode(tiles_shader, camera),
+ InputNode(tiles_shader, environment),
+ DrawNode(tiles_batch)
+ )
+ ),
+ FuncNode(update_tests, (blob_translation, blob_spawn_translation, cube_translation, cube_spawn_translation, cube_orientation, clouds_orientation)),
+ TextureNode((tests_texture, heightmap, normalmap),
+ ShaderNode(tests_shader,
+ InputNode(tests_shader, camera),
+ InputNode(tests_shader, environment),
+ DrawNode(tests_batch)
+ )
+ ),
+ FuncNode(update_sea, (camera, sea_phase)),
+ TextureNode((sea_polar_textures, sea_detail_texture),
+ ShaderNode(sky_shader,
+ InputNode(sky_shader, camera),
+ InputNode(sky_shader, environment),
+ InputNode(sky_shader, sea_phase),
+ DrawNode(sky_triangles)
+ )
+ )
+ )
print("Running... Ctrl+c to quit")
- start_time = time.monotonic()
- current_time = 0.0
- frame = 0
- frame_min = 10000.0
- frame_max = 0.0
- frame_avg = 0.0
- draw_min = 10000.0
- draw_max = 0.0
- draw_avg = 0.0
- perf_count = 0
+ time = Time()
try:
- while True:
- current_time = time.monotonic() - start_time
- frame_begin = time.thread_time()
-
+ while not keyboard.quit:
events.update()
- if keyboard.quit:
- break
-
begin_frame()
-
- camera_distance = mouse.wheel * 20.0
- camera_yaw = mouse.drag[0] * 0.001
- camera_pitch = mouse.drag[1] * 0.001 + pi * 0.25
- camera.set_view(camera_yaw, camera_pitch, camera_distance)
- environment.from_sun(camera.view, sun_direction, sun_power)
-
- terrain_shader.select()
- camera.set_inputs(terrain_shader)
- environment.set_inputs(terrain_shader)
- select_texture(0, tiles_texture)
- select_texture(1, heightmap)
- select_texture(2, normalmap)
- draw_begin = time.thread_time()
- terrain_batch.draw()
- draw_end = time.thread_time()
- unselect_texture(0, tiles_texture)
- unselect_texture(1, heightmap)
- unselect_texture(2, normalmap)
- terrain_shader.unselect()
-
- rotation = mat3()
- mat3_rotation(rotation, vec3_up, (current_time * 0.21) % tau)
- mat3_mul_vec3(blob_translation, rotation, blob_spawn_translation)
- mat3_mul_vec3(cube_translation, rotation, cube_spawn_translation)
- mat3_rotation(cube_orientation, vec3_up, (current_time * 0.43) % tau)
- mat3_rotation(clouds_orientation, vec3_up, (current_time * -0.037) % tau)
-
- tests_shader.select()
- camera.set_inputs(tests_shader)
- environment.set_inputs(tests_shader)
- select_texture(0, tests_texture)
- select_texture(1, heightmap)
- select_texture(2, normalmap)
- tests_batch.draw()
- unselect_texture(0, tests_texture)
- unselect_texture(1, heightmap)
- unselect_texture(2, normalmap)
- tests_shader.unselect()
-
- camera.to_km()
-
- sky_shader.select()
- camera.set_inputs(sky_shader)
- environment.set_inputs(sky_shader)
- set_input_float(sky_shader.u_sea_phase, (current_time * 0.023) % 1.0)
- select_texture(0, sea_polar_textures)
- select_texture(1, sea_detail_texture)
- draw_triangles(sky_triangles)
- unselect_texture(0, sea_polar_textures)
- unselect_texture(1, sea_detail_texture)
- sky_shader.unselect()
-
- frame_end = time.thread_time()
+ time.update()
+ scene.draw(time)
end_frame()
swap_buffers(display)
-
- if frame > 0:
- draw_ms = draw_end - draw_begin
- draw_min = min(draw_min, draw_ms)
- draw_max = max(draw_max, draw_ms)
- draw_avg += draw_ms
- frame_ms = frame_end - frame_begin
- frame_min = min(frame_min, frame_ms)
- frame_max = max(frame_max, frame_ms)
- frame_avg += frame_ms
- perf_count += 1
- frame += 1
-
except KeyboardInterrupt:
pass
- print("\rDraw *", perf_count,
- ": min =", round(draw_min * 1000.0, 2),
- ", max =", round(draw_max * 1000.0, 2),
- ", avg =", round((draw_avg / perf_count) * 1000.0, 2), "ms")
-
- print("\rFrame *", perf_count,
- ": min =", round(frame_min * 1000.0, 2),
- ", max =", round(frame_max * 1000.0, 2),
- ", avg =", round((frame_avg / perf_count) * 1000.0, 2), "ms")
-
- # seed 666
- # for _ in range(10000):
- # current_time = 0 # time.monotonic() - start_time
-
- # Draw * 9999 : min = 0.14 , max = 0.43 , avg = 0.19 ms
- # Draw * 9999 : min = 0.14 , max = 0.35 , avg = 0.19 ms
- # Draw * 9999 : min = 0.13 , max = 0.44 , avg = 0.18 ms
-
- # Frame * 9999 : min = 0.21 , max = 0.7 , avg = 0.33 ms
- # Frame * 9999 : min = 0.2 , max = 0.54 , avg = 0.31 ms
- # Frame * 9999 : min = 0.19 , max = 0.6 , avg = 0.29 ms
-
print("Quitting...")
del tests_batch
- del terrain_batch
- destroy_texture(sea_polar_textures)
- destroy_texture(sea_detail_texture)
- destroy_texture(heightmap)
- destroy_texture(normalmap)
- destroy_triangles(sky_triangles)
+ del tiles_batch
+ del sky_triangles
+ del sea_polar_textures
+ del sea_detail_texture
+ del heightmap
+ del normalmap
archive.destroy()
- del terrain_shader
+ del tiles_shader
del tests_shader
del sky_shader
render_terminate()
diff --git a/game/inputs.py b/game/inputs.py
new file mode 100644
index 0000000..b4616d3
--- /dev/null
+++ b/game/inputs.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2022 RozK
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from engine import set_input_float
+
+class InputFloat:
+ __slots__ = '_input', 'value'
+
+ def __init__(self, input, value):
+ self._input = input
+ self.value = value
+
+ def update(self, value):
+ self.value = value
+
+ def set_inputs(self, shader):
+ set_input_float(self._input, self.value)
diff --git a/game/resources.py b/game/resources.py
index 2982503..98848f6 100644
--- a/game/resources.py
+++ b/game/resources.py
@@ -20,6 +20,7 @@ from pathlib import Path
import png
import engine
+from game.texture import Texture
VERTEX_SIZE = 20
VERTEX_FORMAT = engine.vertex_format(
@@ -133,7 +134,7 @@ class TextureData:
_write_blob(file, self.pixels)
def create_texture(data):
- return engine.create_texture(data.format, data.width, data.height, data.nlevels, data.flags, data.pixels)
+ return Texture(data.format, data.width, data.height, data.nlevels, data.flags, data.pixels)
class VerticesData:
__slots__ = 'name', 'vertices', 'indices'
@@ -208,8 +209,6 @@ class Archive:
def destroy(self):
for vertices in self.vertices_db.values():
engine.destroy_vertices(vertices)
- for texture in self.textures_db.values():
- engine.destroy_texture(texture)
self.textures_db.clear()
self.vertices_db.clear()
self.models_db.clear()
diff --git a/game/scene.py b/game/scene.py
new file mode 100644
index 0000000..a52354c
--- /dev/null
+++ b/game/scene.py
@@ -0,0 +1,95 @@
+# Copyright (C) 2022 RozK
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+class Node:
+ __slots__ = '_subnodes'
+
+ def __init__(self, *subnodes):
+ self._subnodes = subnodes
+
+ def draw(self, time):
+ for subnode in self._subnodes:
+ subnode.draw(time)
+
+class SceneNode(Node):
+ __slots__ = '_subnodes'
+
+ def __init__(self, *subnodes):
+ Node.__init__(self, *subnodes)
+ self._subnodes = subnodes
+
+ def draw(self, time):
+ Node.draw(self, time)
+
+class TextureNode(Node):
+ __slots__ = '_textures'
+
+ def __init__(self, textures, *subnodes):
+ Node.__init__(self, *subnodes)
+ self._textures = textures
+
+ def draw(self, time):
+ for slot, texture in enumerate(self._textures):
+ texture.select(slot)
+ Node.draw(self, time)
+ for slot, texture in enumerate(self._textures):
+ texture.unselect(slot)
+
+class ShaderNode(Node):
+ __slots__ = '_shader'
+
+ def __init__(self, shader, *subnodes):
+ Node.__init__(self, *subnodes)
+ self._shader = shader
+
+ def draw(self, time):
+ self._shader.select()
+ Node.draw(self, time)
+ self._shader.unselect()
+
+class InputNode(Node):
+ __slots__ = '_shader', '_input'
+
+ def __init__(self, shader, input, *subnodes):
+ Node.__init__(self, *subnodes)
+ self._shader = shader
+ self._input = input
+
+ def draw(self, time):
+ self._input.set_inputs(self._shader)
+ Node.draw(self, time)
+
+class DrawNode(Node):
+ __slots__ = '_drawable'
+
+ def __init__(self, drawable, *subnodes):
+ Node.__init__(self, *subnodes)
+ self._drawable = drawable
+
+ def draw(self, time):
+ self._drawable.draw()
+ Node.draw(self, time)
+
+class FuncNode(Node):
+ __slots__ = '_func', '_params'
+
+ def __init__(self, func, params, *subnodes):
+ Node.__init__(self, *subnodes)
+ self._func = func
+ self._params = params
+
+ def draw(self, time):
+ self._func(time, *self._params)
+ Node.draw(self, time)
diff --git a/game/sea.py b/game/sea.py
index 8815b4b..bfed466 100644
--- a/game/sea.py
+++ b/game/sea.py
@@ -17,10 +17,10 @@ from itertools import product
from math import tau, cos, sin, copysign
from array import array
-from engine import (create_texture,
- TEXTURE_FORMAT_RGB10_A2, TEXTURE_FORMAT_TYPECODE, TEXTURE_FLAG_MIN_LINEAR, TEXTURE_FLAG_MAG_LINEAR)
+from engine import TEXTURE_FORMAT_RGB10_A2, TEXTURE_FORMAT_TYPECODE, TEXTURE_FLAG_MIN_LINEAR, TEXTURE_FLAG_MAG_LINEAR
from game.math import vec3_scale, vec3_direction, vec3_cross, vec3_normal_rgb10a2
+from game.texture import Texture
from game.resources import load_png
_format = TEXTURE_FORMAT_RGB10_A2
@@ -51,7 +51,7 @@ def load_polar_textures(paths, waves_height = 0.008):
_width, _height, normals = load_texture(path)
assert _width == width and _height == height
data.extend(list(map(_conv, normals)))
- return create_texture(_format, width, height, len(paths), _flags, data)
+ return Texture(_format, width, height, len(paths), _flags, data)
def load_detail_texture(path, scale = 0.5, waves_height = 0.002):
width, height, data = load_png(path)
@@ -68,4 +68,4 @@ def load_detail_texture(path, scale = 0.5, waves_height = 0.002):
return vec3_scale(n, copysign(1.0, n[2]))
normals = list(map(normal, product(range(height), range(width)), data))
data = array(_typecode, list(map(_conv, normals)))
- return create_texture(_format, width, height, 0, _flags, data)
+ return Texture(_format, width, height, 0, _flags, data)
diff --git a/game/texture.py b/game/texture.py
new file mode 100644
index 0000000..19a507c
--- /dev/null
+++ b/game/texture.py
@@ -0,0 +1,31 @@
+# Copyright (C) 2022 RozK
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from engine import create_texture, select_texture, unselect_texture, destroy_texture
+
+class Texture:
+ __slots__ = '_texture'
+
+ def __init__(self, format, width, height, nlevels, flags, pixels):
+ self._texture = create_texture(format, width, height, nlevels, flags, pixels)
+
+ def __del__(self):
+ destroy_texture(self._texture)
+
+ def select(self, slot):
+ select_texture(slot, self._texture)
+
+ def unselect(self, slot):
+ unselect_texture(slot, self._texture)
diff --git a/game/time.py b/game/time.py
new file mode 100644
index 0000000..e2bc2bb
--- /dev/null
+++ b/game/time.py
@@ -0,0 +1,29 @@
+# Copyright (C) 2022 RozK
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see .
+
+from time import monotonic
+
+class Time:
+ __slots__ = '_start', 'current', 'delta'
+
+ def __init__(self):
+ self._start = monotonic()
+ self.current = 0.0
+ self.delta = 0.0
+
+ def update(self):
+ current = monotonic() - self._start
+ self.delta = current - self.current
+ self.current = current
diff --git a/game/triangles.py b/game/triangles.py
index 82c3c96..a5838db 100644
--- a/game/triangles.py
+++ b/game/triangles.py
@@ -16,35 +16,49 @@
from itertools import chain
from array import array
from math import cos, sin
+from engine import create_triangles, draw_triangles, destroy_triangles
-# TODO: with FOV
-def sky_triangles(vsubdivs, distance, projection_ratio):
- assert vsubdivs > 0
- vertices = []
- hsubdivs = round(vsubdivs * projection_ratio)
- z = -distance
- width = distance * projection_ratio
- height = distance
- startx = width * -0.5
- starty = height * -0.5
- stepx = width / hsubdivs
- stepy = height / vsubdivs
- y1 = starty
- y2 = y1 + stepy
- for sy in range(vsubdivs):
- x1 = startx
- x2 = x1 + stepx
- for sx in range(hsubdivs):
- a = (x1, y2, z)
- b = (x2, y1, z)
- vertices.append((x1, y1, z))
- vertices.append(b)
- vertices.append(a)
- vertices.append((x2, y2, z))
- vertices.append(a)
- vertices.append(b)
- x1 = x2
- x2 += stepx
- y1 = y2
- y2 += stepy
- return array('f', chain.from_iterable(vertices))
+class Triangles:
+ __slots__ = '_triangles'
+
+ def __init__(self, triangles):
+ self._triangles = create_triangles(triangles)
+
+ def __del__(self):
+ destroy_triangles(self._triangles)
+
+ def draw(self):
+ draw_triangles(self._triangles)
+
+class SkyTriangles(Triangles):
+ # TODO: with FOV
+ def __init__(self, vsubdivs, distance, projection_ratio):
+ assert vsubdivs > 0
+ vertices = []
+ hsubdivs = round(vsubdivs * projection_ratio)
+ z = -distance
+ width = distance * projection_ratio
+ height = distance
+ startx = width * -0.5
+ starty = height * -0.5
+ stepx = width / hsubdivs
+ stepy = height / vsubdivs
+ y1 = starty
+ y2 = y1 + stepy
+ for sy in range(vsubdivs):
+ x1 = startx
+ x2 = x1 + stepx
+ for sx in range(hsubdivs):
+ a = (x1, y2, z)
+ b = (x2, y1, z)
+ vertices.append((x1, y1, z))
+ vertices.append(b)
+ vertices.append(a)
+ vertices.append((x2, y2, z))
+ vertices.append(a)
+ vertices.append(b)
+ x1 = x2
+ x2 += stepx
+ y1 = y2
+ y2 += stepy
+ super().__init__(array('f', chain.from_iterable(vertices)))