Quick Links: Download Gideros Studio | Gideros Documentation | Gideros community chat | DONATE
Is it possible in Gideros to work with separate polygons and vertices in 3d? — Gideros Forum

Is it possible in Gideros to work with separate polygons and vertices in 3d?

Hi guys
I accidentally learned an interesting method of creating a game like Minecraft
For optimization, the game does not draw each individual cube, but combines blocks of cubes (chunks) 16 * 16 * 16 cubes into separate 3D models.
So when you add a new cube to the world, you don't add a new 3D model, you just move the chunk's polygons.

I hope that the Google translator correctly translated my opinion.

MY QUESTION - is it possible to use such technology in GIDEROS? Is it possible to move individual polygons and combine them into 3d objects?


I saw the following technique in this guide:

Likes: MoKaLux

my games:
https://play.google.com/store/apps/developer?id=razorback456
мій блог по гідерос https://simartinfo.blogspot.com
Слава Україні!
+1 -1 (+1 / -0 )Share on Facebook

Comments

  • olegoleg Member
    edited November 2021
    keszegh said:

    i think it is just maths so not gideros specific.

    I'm not asking about math - there are no problems with that, I ask. 'Can I move vertices and polygons in 3D models' by means of API Gideos

    I just did not work with 3d in gideros, I know that it can add ready-made 3D objects. But I don't know if it is possible not to add objects and to create in the middle of gideros.
    my games:
    https://play.google.com/store/apps/developer?id=razorback456
    мій блог по гідерос https://simartinfo.blogspot.com
    Слава Україні!
  • i see, for that i don't know the answer but i guess creating a new object and deleting the old is not that much slower than modifying it, at least up to some complexity.
  • oleg, I think it is possible see https://forum.gideros.rocks/discussion/7879/3d-landscape-sample-code-and-an-odd-graphical-artifact

    Unfortunatelly the link to the zip file is broken. I have it on my computer for you to explore, how can I send it to you?

    excerpt:
    --[[
    landscape.lua
    Implements a 3D landscape as defined by a heightfield texture and scale parameters.
    Note: This module requires the LUA file system (LFS) plugin.
    --]]
     
    require 'lfs'
     
    Landscape = gideros.class(Sprite)
     
    local function interpolate(low, high, high_ratio) return low + (high - low) * high_ratio end
     
    function Landscape:init(heightfield_image_path, vertices_per_side, world_size, min_elevation, max_elevation, texture_file, clean_edges)
    	--[[
    	Input parameters:
    	heightfield_image_path - Image of heightfield, with light shades for high elevaltions, dark for lowest_loc_x
    	vertices_per_side - Number of vertices in each row and column of the mesh. 128 is reasonable. The resulting mesh
    		will have a total vertex count of the square of this number.
    	world_size - In world units, which could be treated as feet, meters, cm, etc. 10000 is managed, representing
    		close to 4 square miles, if a unit is a foot.  Mesh will be centered around 0X and 0Z,
    		with 1/2 world_size extending from the origin on each horizontal axis.
    	min_elevation, max_elevation - The elevation, in world units, of the lowest and highest points.
    	texture_file - Path to the texture to apply to the landscape
    	clean_edges - If set, lower vertices on edges to the lowest elevation, to prevent exposing edges.
    	--]]
    	-- Store input parameters:
    	self.world_size = world_size
    	self.vertices_per_side = vertices_per_side
    	self.texture_file = texture_file
    	-- Create a folder for temporary files:
    	local temp_obj_folder = "|D|landscape_temp"
    	lfs.mkdir(temp_obj_folder)
    	-- Make a copy of the texture in the temp folder:
    	self.landscape_texture = Texture.new(texture_file)
    	local rt = RenderTarget.new(self.landscape_texture:getWidth(), self.landscape_texture:getHeight())
    	local bmp = Bitmap.new(self.landscape_texture)
    	rt:draw(bmp)
    	local temp_texture_file_name = "temp_texture.png"
    	rt:save(temp_obj_folder .. "/" .. temp_texture_file_name)
    	-- Load the heightmap texture:
    	local heightmap_texture = Texture.new(heightfield_image_path)
    	-- Store the dimensions:
    	local map_w = heightmap_texture:getWidth()
    	local map_h = heightmap_texture:getHeight()
    	-- Create a render target so we can access the heightfield pixel data:
    	local rt = RenderTarget.new(self.vertices_per_side, self.vertices_per_side)
    	local bmp = Bitmap.new(heightmap_texture)
    	-- Scale the heightfield if necessary to fit the render target:
    	if map_w ~= self.vertices_per_side or map_h ~= self.vertices_per_side then
    		bmp:setScaleX(self.vertices_per_side / bmp:getWidth())
    		bmp:setScaleY(self.vertices_per_side / bmp:getHeight())
    	end
    	-- Populate the render target with the map:
    	rt:draw(bmp)
    	-- Build a height table with an entry for each pixel in the height map:
    	self.height_table = {}
    	local buffer = rt:getPixels()
    	-- Keep track of the darkest and lightest pixels we find so we can
    	-- calculate the range between them and scale vertically to the
    	-- desired range:
    	local max_shade = -999;
    	local min_shade = 999;
    	self.lowest_loc_x = 0;
    	self.lowest_loc_z = 0;
    	self.highest_loc_x = 0;
    	self.highest_loc_z = 0;
    	-- Make one pass through the pixels to find the lightest and darkest:
    	for x = 0, self.vertices_per_side - 1 do
    		-- Note: Y coordinate in texture will correspond to Z coordinate in resulting model, with the
    		-- Y axis representing elevation.
    		for y = 0, self.vertices_per_side - 1 do
    			-- Index assumes 4 bytes per pixel. Index is for red component, all we need for height
    			local index = (y * self.vertices_per_side + x) * 4 + 1
    			local color = string.byte(buffer, index) -- (x, y)
    			if color < min_shade then
    				min_shade = color
    				self.lowest_loc_x = x;
    				self.lowest_loc_z = y;
    			end
    			if color > max_shade then
    				max_shade = color
    				self.highest_loc_x = x;
    				self.highest_loc_z = y;
    			end
    		end
    	end -- of loop through x coordinates
     
    	local shade_range = max_shade - min_shade 
     
    	-- Make another pass through the pixels, computing the height each represents.
    	for x = 0, self.vertices_per_side - 1 do
    		self.height_table[x] = {}
    		-- Note: Y coordinate in texture will correspond to Z coordinate in resulting model, with the
    		-- Y axis representing elevation.
    		for y = 0, self.vertices_per_side - 1 do
    			-- Index assumes 4 bytes per pixel. Index is for red component, all we need for height
    			local index = (y * self.vertices_per_side + x) * 4 + 1
    			local color = string.byte(buffer, index)
    			self.height_table[x][y] = (color - min_shade) / shade_range * (max_elevation - min_elevation) + min_elevation
    			if clean_edges then
    				if x == 0 or y == 0 or x == self.vertices_per_side - 1 or y == self.vertices_per_side - 1 then
    					self.height_table[x][y] = min_elevation
    				end
    			end
    		end
    	end -- of loop through x coordinates
     
    	-- We have a table of heights from the heightfield map.  Now creat an .obj
    	-- file defining a mesh of the landscape:
    	local temp_obj_file_name = "landscape_generated_model.obj"
    	file = io.open(temp_obj_folder .. "/" .. temp_obj_file_name, "w")
    	file:write("mtllib landscape.mtl\n")
    	file:write("o Cube\n")
    	-- Create vertexes:
    	for x = 0, self.vertices_per_side - 1 do
    		for z = 0, self.vertices_per_side - 1 do
    			local vy = self.height_table[x][z]
    			local vx = ((x / (self.vertices_per_side - 1)) - 0.5) * self.world_size
    			local vz = ((z / (self.vertices_per_side - 1)) - 0.5) * self.world_size
    			file:write("v "
    				.. string.format("%1.06f", vx) .. " "
    				.. string.format("%1.06f", vy) .. " "
    				.. string.format("%1.06f", vz) .. "\n")
    		end
    	end -- of loop through x coordinates
    	-- Create vertex texture coordinates:
    	for x = 0, self.vertices_per_side - 1 do
    		for z = 0, self.vertices_per_side - 1 do
    			local vu, vv = -- vertex u and v, coordinates into the texture:
    				-- Normal, works:
    				x / (self.vertices_per_side - 1),  -- assumes texture of same size as map.
    				z / (self.vertices_per_side - 1)
    			-- vu = (x % 2) --  % 2 * n repeats the whole texture N times per face
    			-- vv = (z % 2) -- well, %2 works, but %2 * n does the texture once, and stripes at edges beyond that.
     
    			file:write("vt "
    				.. string.format("%1.06f", vu) .. " "
    				.. string.format("%1.06f", vv) .. "\n")
    		end
    	end -- of loop through x coordinates
    	-- Create normals, one for each vertex:
    	for x = 0, self.vertices_per_side - 1 do
    		for z = 0, self.vertices_per_side - 1 do
    			local vx, vy, vz = 0, 1, 0  -- Default normal to facing straight up
     
    			local y1 = self.height_table[x][z]
    			local y2 = y1 -- Default to same value
    			local y3 = y1 -- Default to same value
    			if self.height_table[x + 1] and self.height_table[x + 1][z] then
    				y2 = self.height_table[x + 1][z]
    			end
    			if self.height_table[x] and self.height_table[x][z + 1] then
    				y3 = self.height_table[x][z + 1]
    			end
    			local anglex = 0
    			if y1 and y2 and y3 then
    				anglex = math.atan(y2 - y1)
    				vx = math.sin(anglex)
    				local anglez = math.atan(y3 - y1)
    				vz = math.sin(anglez)
    				vy = math.cos(anglex * anglez)
    			end
    			-- Convert the normal to a unit vector:
    			local length = math.sqrt(vx * vx + vy * vy + vz * vz)
    			vx = vx / length
    			vy = vy / length
    			vz = vz / length
    			-- Write the vertex normal:
    			file:write("vn "
    				.. string.format("%1.06f", vx) .. " "
    				.. string.format("%1.06f", vy) .. " "
    				.. string.format("%1.06f", vz) .. "\n")
    		end
    	end -- of loop through x coordinates
    	file:write("usemtl Material\n")
    	file:write("s off\n") -- smoothing across polygons, 1 or off.
    	-- Create faces, one fewer than vertices in each row and column::
    	for x = 1, self.vertices_per_side - 1 do
    		for z = 1, self.vertices_per_side - 1 do
    			-- Starting vertex index
    			local v1 = x + (z - 1) * (self.vertices_per_side) -- herenow findmenow  - still in synch w/ vertices?
    			-- Link to the vertex to the right, x+1
    			-- Don't have to worry about edges - we have one more vertex than face in each row
    			local v2 = v1 + 1
    			-- Next is one down, at X+1, Z+1
    			local v3 = (x + 1) + ((z + 1) - 1) * (self.vertices_per_side)
    			-- Next is back to X, down at Z+1
    			local v4 = x + ((z + 1) - 1) * (self.vertices_per_side)
    			-- second and third value for each face is the same - UV coordinates and normals listed in same order as vertexes.
    			file:write("f "
    			.. v1 .. "/" .. v1 .. "/" .. v1 .. " "
    			.. v2 .. "/" .. v2 .. "/" .. v2 .. " "
    			.. v3 .. "/" .. v3 .. "/" .. v3 .. " "
    			.. v4 .. "/" .. v4 .. "/" .. v4 .. "\n")
    		end
    	end -- of loop through x coordinates
    	file:close()
    	local material_file_contents =
    [[
    newmtl Material
    Ns 1000
    Ka 1.000000 1.000000 1.000000
    Kd 0.640000 0.640000 0.640000
    Ks 0.500000 0.500000 0.500000
    Ke 0.000000 0.000000 0.000000
    Ni 1.000000
    d 1.000000
    illum 2
    map_Kd ]]
    .. temp_texture_file_name ..
    [[
     
    ]]
    	file = io.open(temp_obj_folder .. "/" .."landscape.mtl", "w")
    	file:write(material_file_contents)
    	file:close()
    	-- We now have a .obj file.  Create the mesh:
    	self.sprite = loadObj(temp_obj_folder, temp_obj_file_name)
    	-- This class extends Sprite, so add the model as a child of this instance:
    	self:addChild(self.sprite)
    	-- Store a reference to the object in an objs table for compatibility with shaders based on lighting.lua
    	self.objs = {}
    	table.insert(self.objs, self.sprite)
    end -- of :init()
     
    function Landscape:getElevation(x, z)
    	-- Return the elevation in world units at specified world coordinates, interpolating between
    	-- points defined by the heightfield as appropriate.
     
    	-- Get the relative position in the height table represented by the coordinates:
    	local rx = math.max(math.min((x - (0 - self.world_size / 2)) / self.world_size, 1), 0)
    	local rz = math.max(math.min((z - (0 - self.world_size / 2)) / self.world_size, 1), 0)
    	-- Coords would be indexes into table, but are not integer. Floor will be low idx, floor + 1 will be high idx
    	local x_coord = rx * (self.vertices_per_side - 1)
    	local z_coord = rz * (self.vertices_per_side - 1)
    	-- Get indexes for the height table entries that surround the point in question
    	local low_x_idx = math.max(0, math.floor(x_coord))
    	local high_x_idx = math.min(low_x_idx + 1, self.vertices_per_side - 1)
    	local low_z_idx = math.max(0, math.floor(z_coord))
    	local high_z_idx = math.min(low_z_idx + 1, self.vertices_per_side - 1) 
    	-- Determine how far we'll interpolate between on X and Z axis
    	local x_interp = x_coord % 1
    	local z_interp = z_coord % 1
    	-- Get the height at those four points, starting with safe defaults:
    	local height00 = 0
    	local height01 = 0
    	local height10 = 0
    	local height11 = 0
    	if self.height_table[low_x_idx] then
    		height00 = self.height_table[low_x_idx][low_z_idx]
    		height01 = self.height_table[low_x_idx][high_z_idx]
    		if not height00 then height00 = 0 end
    		if not height01 then height01 = 0 end
    	end
    	if self.height_table[high_x_idx] then
    		height10 = self.height_table[high_x_idx][low_z_idx]
    		height11 = self.height_table[high_x_idx][high_z_idx]
    		if not height10 then height10 = 0 end
    		if not height11 then height11 = 0 end
    	end
    	-- Interpolate across the top row (min y, from low to high x)
    	local height_top = interpolate(height00, height10, x_interp)
    	-- And on the bottom row:
    	local height_bottom = interpolate(height01, height11, x_interp)
    	-- And between them by y interp:
    	local val = interpolate(height_top, height_bottom, z_interp)
     
    	return val
    end
     
    function Landscape:getSlope(x, z, relative_direction)
    	local offset_x1 = 1
    	local offset_z1 = 0
    	local offset_x2 = 0
    	local offset_z2 = 1
     
    	if relative_direction then
    		offset_x = 0 - math.cos(math.rad(relative_direction))
    		offset_z = math.sin(math.rad(relative_direction))
    		offset_x1 = offset_x
    		offset_z1 = offset_z
    		offset_x2 = -offset_z
    		offset_z2 = offset_x
    	end
    	local h = self:getElevation(x, z)
    	local hx1 = self:getElevation(x + offset_x1, z + offset_z1)
    	local hz1 = self:getElevation(x + offset_x2, z + offset_z2)
     
    	local slope_x = math.deg(math.atan(hx1 - h))
    	local slope_z = math.deg(math.atan(hz1 - h))
     
    	return slope_x, slope_z
    end
    my growING GIDEROS github repositories: https://github.com/mokalux?tab=repositories
  • ok zip files are working again :p
    my growING GIDEROS github repositories: https://github.com/mokalux?tab=repositories
  • ok zip files are working again :p
    nope zip files don't work on the forum :/
    my growING GIDEROS github repositories: https://github.com/mokalux?tab=repositories
  • MoKaLux said:

    ok zip files are working again :p
    nope zip files don't work on the forum :/

    Thank you, but you misunderstood me -I don't need to generate a landscape, I know how to do it, I need to optimize the geometry to reduce the number of polygons.
    my games:
    https://play.google.com/store/apps/developer?id=razorback456
    мій блог по гідерос https://simartinfo.blogspot.com
    Слава Україні!
  • keszegh said:

    i see, for that i don't know the answer but i guess creating a new object and deleting the old is not that much slower than modifying it, at least up to some complexity.

    I once did a clone of minecraft with individual cubes -it turns out very unoptimized.

    The first problem is the removal of invisible polygons.
    And with each renewal of the world we spend a lot of resources
    The second problem is the inability to create an LOD, because a cube is the simplest form

    The system with chunks works very well because if the chunk is a separate 3D model, instead of 4096 cubes you can display 1 cube on the chunks farthest from the player. And change the LOD to another as the player approaches them..

    On "cgpeers" there are full lessons, if you are interested to see



    https://www.minecraftforum.net/forums/minecraft-java-edition/suggestions/2996005-dynamic-lod-more-chunks-at-a-lower-price
    my games:
    https://play.google.com/store/apps/developer?id=razorback456
    мій блог по гідерос https://simartinfo.blogspot.com
    Слава Україні!
  • hgy29hgy29 Maintainer
    Accepted Answer
    Oleg, I think Gideros can do it, basically Gideros doesn’t understand the concept of model at all, it only deals with bare meshes, that it is a set of vertices and indices (the Mesh Sprite), for which you set the indices and vertices in lua. You could have vertices precomputed to form a 3D grid, and only change the indices dynamically to display the relevant triangles depending on LOD.
    I think it could even be done in the vertex shader, by a clever use of instancing.
    Btw, I plan to implement back face culling in Gideros for meshes.
    +1 -1 (+3 / -0 )Share on Facebook
  • olegoleg Member
    edited November 2021
    hgy29 said:

    Oleg, I think Gideros can do it, basically Gideros doesn’t understand the concept of model at all, it only deals with bare meshes, that it is a set of vertices and indices (the Mesh Sprite), for which you set the indices and vertices in lua. You could have vertices precomputed to form a 3D grid, and only change the indices dynamically to display the relevant triangles depending on LOD.
    I think it could even be done in the vertex shader, by a clever use of instancing.
    Btw, I plan to implement back face culling in Gideros for meshes.

    By the concept of the model -I mean the mesh. Each chunk 16 * 16 * 16 is a separate "mesh"
    If you combine the cubes into one mesh(chunk)- then inside there will be internal faces - which need to be dynamically removed or waited when the chunk changes.

    It's just translation difficulties between different languages

    Rejection of back faces - it is not always necessary, for example on a grass and leaves on trees - faces do not need to be rejected. -I think it will be possible to choose?
    my games:
    https://play.google.com/store/apps/developer?id=razorback456
    мій блог по гідерос https://simartinfo.blogspot.com
    Слава Україні!
  • hgy29hgy29 Maintainer
    oleg said:


    Rejection of back faces - it is not always necessary, for example on a grass and leaves on trees - faces do not need to be rejected. -I think it will be possible to choose?

    Yes, of course, this will be a per mesh on/off setting.

    Likes: oleg

    +1 -1 (+1 / -0 )Share on Facebook
  • hgy29 said:


    I think it could even be done in the vertex shader, by a clever use of instancing.

    This is a randomly generated world, all meshes will be unique - so instances are hardly possible.

    LODs don't have to be constantly generated, I think they should be stored on disk when the world is generated, and then just read from disk

    Only the "chunk" in which the player is located will change dynamically. And the other 1728 "chunks" (meshes) just have to be loaded from the hard drive.


    my games:
    https://play.google.com/store/apps/developer?id=razorback456
    мій блог по гідерос https://simartinfo.blogspot.com
    Слава Україні!
  • hgy29hgy29 Maintainer
    oleg said:


    This is a randomly generated world, all meshes will be unique - so instances are hardly possible.

    I was thinking of instanced rendering of a single cube, similarly to the instancing demo. Your chunk could made of say, 16x16x16 instances of that cube, with the vertex shader applying translation and selecting color/texture based on a map.
    The vertex shader could also compute the distance of the mesh from the view point, and decide to reduce LOD dynamically by merging cubes together on the fly and culling unused ones (by making degenerate triangles).

    Likes: oleg

    +1 -1 (+1 / -0 )Share on Facebook
  • olegoleg Member
    edited November 2021
    hgy29 said:

    oleg said:


    This is a randomly generated world, all meshes will be unique - so instances are hardly possible.

    I was thinking of instanced rendering of a single cube, similarly to the instancing demo. Your chunk could made of say, 16x16x16 instances of that cube, with the vertex shader applying translation and selecting color/texture based on a map.
    The vertex shader could also compute the distance of the mesh from the view point, and decide to reduce LOD dynamically by merging cubes together on the fly and culling unused ones (by making degenerate triangles).
    Not the whole cube is generated, but only the wall of the cube which is next to the air cube, the walls which are not generated next to other cubes. Then these walls are combined into chunks (meshes, objects) of size 16 * 16 * 16

    Since the world consists of cubic chunks and their ordinal numbering - the distance to the objects for LOD does not need to be calculated. Just the chunk number where the player is +3 = LOD1 +3 = LOD2 + 3 = lod3 + 3 = lod4

    That is 12 chunks from itself - the player will see LOD 4 - where there will be only 1 cube instead of 4096 cubes. And 3 chunks from himself the player will see LOD1 - where there will be 512 cubes instead of 4096.

    I rejected the idea of ​​generating an LOD on the fly - it will be costly if the tailor moves around the world, the world will not have time to generate.

    Although you led me to think that you can do the opposite -generate first a low-polygonal world LOD4, and then randomly divide each cube into 4 cubes. But it will be necessary to think of some system that the same cubes were always divided in the same way
    my games:
    https://play.google.com/store/apps/developer?id=razorback456
    мій блог по гідерос https://simartinfo.blogspot.com
    Слава Україні!
  • I'm sure what you're talking about is possible. The landscape code demonstrates building a mesh dynamically, actually by writing a file that 3DObjLoader can read to build the model. One could skip the intermediate step, and build a mesh a vertex at a time the way 3DObjLoader does. Either way one could procedurally build the mesh such that it only included polygons for the exposed surfaces. The math and code would be non-trivial, at least I'd find it challenging, more complicated than the heightfield landscape, but it's certainly feasible.

    That would give you what you describe - a single mesh with all the exposed faces of all the cubes, or at least the ones in a given region. You could do variable LOD in that too, having the code reduce the number of polygons for very distant chunks of cubes, like having one polygon represent a plane or near-constant slope of cubes. Again, non-trivial in any system, but the biggest challenge might be developing or finding the algorithm to simplify the mesh. But it's perfectly feasible in Gideros.

    Likes: oleg

    +1 -1 (+1 / -0 )Share on Facebook
  • PaulH said:

    but the biggest challenge might be developing or finding the algorithm to simplify the mesh. But it's perfectly feasible in Gideros.

    The algorithm is very simple, I will just delete all the paired cubes horizontally and vertically =)
    my games:
    https://play.google.com/store/apps/developer?id=razorback456
    мій блог по гідерос https://simartinfo.blogspot.com
    Слава Україні!
  • Good point. I was thinking of the algorithm to dynamically build the vertexes, but perhaps it isn't that complicated. You already know the vertexes for each cube, and can just filter the ones that are not exposed. Yeah, pretty straightforward alright.

    I was thinking of trying to optimally reduce the mesh without changing the shape, so if you have a plane, say a layer of 2x2 cubes laying horizontally, you could discard the vertexes where they meet in the middle to have a single surface each for the tops and bottoms of all four. You could also drop the vertexes in the middle of each edge. But removing any vertexes would break the textures, unless you only did that where you could repeat each texture (twice in that example) across the combined surface. It might not be worth making it that sophisticated just to save some polygons. You'd have to do that sort of thing if you were going to simplify the mesh for low LOD, though.

    Ooh, but if you did... Suppose you had a chunk of cubes that basically make up a steady slope - 10 wide, 1 high at the first row, 2 high at the second, etc up to 10. You have over 500 cubes, but only a couple hundred exposed, and roughly 1,000 vertexes. For low LOD you could simplify that mesh down to a simple wedge shape with only 6 vertexes. Then to preserve the texture you could build the mesh in full detail, draw that to a render target, once for each side of it that corresponds to a face of the LOD version. Then on the simplified mesh you could use those rendered images as the textures for each face.

    In the case of a chunk you can represent reasonably well with a cube or wedge, that would be pretty straightforward and would give you very simple meshes that represent lots of cubes quite nicely at a distance. Doing that more generally (for less simple chunk shapes) would get more complicated, but it could be done.
  • PaulH said:


    For LOD - it is not necessary to do it. The task is to see a beautiful landscape from afar -and accuracy is not required at all. Because approaching will load the exact model instead of LOD

    1 generated game level - weighs about a gigabyte on the hard drive - if you generate unique textures - then no media will have enough space for this file size.

    Also together with LOD, mipmaping will be used for texture - therefore the texture needs to be left by tiles

    The farthest LODs will be completely textured and filled with vertex color.





    my games:
    https://play.google.com/store/apps/developer?id=razorback456
    мій блог по гідерос https://simartinfo.blogspot.com
    Слава Україні!
Sign In or Register to comment.