Binding OpenGL vertex buffers per frame for multiple meshes - c#

I'm making and OpenGL application that has MULTIPLE meshes that are described as lists of positions, normals, and uvs. I am binding these data to a vertex buffer but I was wondering how I would draw these meshes per frame without re-binding the vertex buffer. Correct me if I'm wrong, but isn't copying ~100KB of data to the vertex buffer slowish? How would I draw each mesh with separate transforms (position, rotation, scale). Thanks :) Here is my Mesh code:
using System;
using System.IO;
using OpenTK;
using OpenTK.Graphics.OpenGL;
public class Mesh
{
public Vector3[] positions;
public Vector3[] normals;
public Vector2[] uvs;
public Triangle[] triangles;
public int buffer;
public Mesh()
{
this.positions = new Vector3[0];
this.normals = new Vector3[0];
this.uvs = new Vector2[0];
this.triangles = new Triangle[0];
this.buffer = 0;
}
public Mesh(Vector3[] positions, Vector3[] normals, Vector2[] uvs, Triangle[] triangles, int buffer)
{
this.positions = positions;
this.normals = normals;
this.uvs = uvs;
this.triangles = triangles;
this.buffer = buffer;
}
public static Mesh fromFile(string fileName)
{
Mesh mesh = new Mesh();
BinaryReader binaryReader = new BinaryReader(new FileStream(fileName, FileMode.Open));
int positionCount = binaryReader.ReadInt32();
mesh.positions = new Vector3[positionCount];
for (int i = 0; i < positionCount; i++)
{
mesh.positions[i] = new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
}
int normalCount = binaryReader.ReadInt32();
mesh.normals = new Vector3[normalCount];
for (int i = 0; i < normalCount; i++)
{
mesh.normals[i] = new Vector3(binaryReader.ReadSingle(), binaryReader.ReadSingle(), binaryReader.ReadSingle());
}
int uvCount = binaryReader.ReadInt32();
mesh.uvs = new Vector2[uvCount];
for (int i = 0; i < uvCount; i++)
{
mesh.uvs[i] = new Vector2(binaryReader.ReadSingle(), binaryReader.ReadSingle());
}
int triangleCount = binaryReader.ReadInt32();
mesh.triangles = new Triangle[triangleCount];
for (int i = 0; i < triangleCount; i++)
{
mesh.triangles[i] = new Triangle(binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadInt32(), binaryReader.ReadInt32());
}
binaryReader.Close();
return mesh;
}
public void toFile(string fileName)
{
BinaryWriter binaryWriter = new BinaryWriter(new FileStream(fileName, FileMode.OpenOrCreate));
binaryWriter.Write(positions.Length);
for (int i = 0; i < positions.Length; i++)
{
binaryWriter.Write(positions[i].X);
binaryWriter.Write(positions[i].Y);
binaryWriter.Write(positions[i].Z);
}
binaryWriter.Write(normals.Length);
for (int i = 0; i < normals.Length; i++)
{
binaryWriter.Write(normals[i].X);
binaryWriter.Write(normals[i].Y);
binaryWriter.Write(normals[i].Z);
}
binaryWriter.Write(uvs.Length);
for (int i = 0; i < uvs.Length; i++)
{
binaryWriter.Write(uvs[i].X);
binaryWriter.Write(uvs[i].Y);
}
binaryWriter.Write(triangles.Length);
for (int i = 0; i < triangles.Length; i++)
{
binaryWriter.Write(triangles[i].positionIndex0);
binaryWriter.Write(triangles[i].normalIndex0);
binaryWriter.Write(triangles[i].uvIndex0);
binaryWriter.Write(triangles[i].positionIndex1);
binaryWriter.Write(triangles[i].normalIndex1);
binaryWriter.Write(triangles[i].uvIndex1);
binaryWriter.Write(triangles[i].positionIndex2);
binaryWriter.Write(triangles[i].normalIndex2);
binaryWriter.Write(triangles[i].uvIndex2);
}
binaryWriter.Close();
}
public void draw(Transform transform)
{
float[] data = new float[triangles.Length * 24];
for (int i = 0; i < triangles.Length; i++)
{
data[(i * 9) + 0] = positions[triangles[i].positionIndex0].X;
data[(i * 9) + 1] = positions[triangles[i].positionIndex0].Y;
data[(i * 9) + 2] = positions[triangles[i].positionIndex0].Z;
data[(i * 9) + 3] = positions[triangles[i].positionIndex1].X;
data[(i * 9) + 4] = positions[triangles[i].positionIndex1].Y;
data[(i * 9) + 5] = positions[triangles[i].positionIndex1].Z;
data[(i * 9) + 6] = positions[triangles[i].positionIndex2].X;
data[(i * 9) + 7] = positions[triangles[i].positionIndex2].Y;
data[(i * 9) + 8] = positions[triangles[i].positionIndex2].Z;
data[(triangles.Length * 9) + (i * 9) + 0] = normals[triangles[i].normalIndex0].X;
data[(triangles.Length * 9) + (i * 9) + 1] = normals[triangles[i].normalIndex0].Y;
data[(triangles.Length * 9) + (i * 9) + 2] = normals[triangles[i].normalIndex0].Z;
data[(triangles.Length * 9) + (i * 9) + 3] = normals[triangles[i].normalIndex1].X;
data[(triangles.Length * 9) + (i * 9) + 4] = normals[triangles[i].normalIndex1].Y;
data[(triangles.Length * 9) + (i * 9) + 5] = normals[triangles[i].normalIndex1].Z;
data[(triangles.Length * 9) + (i * 9) + 6] = normals[triangles[i].normalIndex2].X;
data[(triangles.Length * 9) + (i * 9) + 7] = normals[triangles[i].normalIndex2].Y;
data[(triangles.Length * 9) + (i * 9) + 8] = normals[triangles[i].normalIndex2].Z;
data[(triangles.Length * 18) + (i * 6) + 0] = uvs[triangles[i].uvIndex0].X;
data[(triangles.Length * 18) + (i * 6) + 1] = uvs[triangles[i].uvIndex0].Y;
data[(triangles.Length * 18) + (i * 6) + 2] = uvs[triangles[i].uvIndex1].X;
data[(triangles.Length * 18) + (i * 6) + 3] = uvs[triangles[i].uvIndex1].Y;
data[(triangles.Length * 18) + (i * 6) + 4] = uvs[triangles[i].uvIndex2].X;
data[(triangles.Length * 18) + (i * 6) + 5] = uvs[triangles[i].uvIndex2].Y;
}
buffer = GL.GenBuffer();
GL.BindBuffer(BufferTarget.ArrayBuffer, buffer);
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(triangles.Length * 96), data, BufferUsageHint.StaticDraw);
//------------------------
//----------TODO----------
//------------------------
}
}
The last function, draw, is the one I'm working on.

The point is to have a single VBO per mesh that you load once and then just rebind as needed.
if you are in openGL 3.3+ you can collect all needed bindings for each mesh in a VAO per mesh: (pseudo-ish code)
class MeshBuffer{
int vao;
int vbo;
int numVertices;
void Dispose(){
if(vao==0)return;
GL.DeleteVertexArrays(1, ref vao);
GL.DeleteBuffers(1, ref vbo);
vao = 0;
vbo = 0;
}
void Bind(){
GL.BindVertexArray(vao);
}
void Unbind(){
GL.BindVertexArray(0);
}
void FillVBO(Mesh mesh){
Dispose();
GL.GenVertexArrays(1, out vao);
GL.GenBuffers(1, out vbo);
float[] data = new float[mesh.triangles.Length * 24];
//your for loop
GL.BindVertexArray(vao);
GL.BindBuffer(BufferTarget.ArrayBuffer, vbo);
GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(triangles.Length * 96), data, BufferUsageHint.StaticDraw);
GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, 0, 0);
GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, 0, triangles.Length * 9*4);
GL.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, 0, triangles.Length * 18*4);
GL.BindVertexArray(0);
GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
}
}
Then to draw you just bind the MeshBuffer and load the transformation matrix into the relevant uniform.
int mvpMat = GL.GetUniformLocation(prog, "mvp");
GL.UseProgram(prog);
meshBuffer.Bind();
GL.UniformMatrix4(mvpMat, transform.Mat4());
GL.DrawArrays(GL_TRIANGLES, 0, meshBuffer.numVertices);

Related

Why does my procedural grid mesh not triangulate properly for grids bigger than 256x256?

I've been making procedural terrain height maps with the diamond square algorithm and the mesh with the triangulation method below:
public Map GenerateMap()
{
Mesh mapMesh = new();
vertices = new Vector3[(Resolution + 1) * (Resolution + 1)];
Vector2[] uv1 = new Vector2[vertices.Length];
Vector2[] uv2 = new Vector2[vertices.Length];
Vector2[] uv3 = new Vector2[vertices.Length];
DiamondSquare diamondSquare = new(Resolution, Roughness, Seed, HeightLevels);
float[,] heightFloatMap = diamondSquare.DoDiamondSquare();
tex = new Texture2D(Resolution, Resolution);
for (int y = 0, i = 0; y <= Resolution; y++)
{
for (int x = 0; x <= Resolution; x++, i++)
{
//float height = heightMap.GetPixel(x,y).r;
float height = heightFloatMap[x, y];
vertices[i] = new Vector3(x * CellSize.x, height * CellSize.y, y * CellSize.z);
tex.SetPixel(x, y, new Color(height, height, height, 1));
if (height == 0)
uv1[i] = new Vector2(vertices[i].x, vertices[i].z);
else if (height < 0.4)
uv2[i] = new Vector2(vertices[i].x, vertices[i].z);
else if (height < 0.4)
uv3[i] = new Vector2(vertices[i].x, vertices[i].z);
}
}
mapMesh.vertices = vertices;
mapMesh.uv = uv1;
mapMesh.uv2 = uv2;
int[] triangles = new int[Resolution * Resolution * 6];
Cell[,] cellMap = new Cell[Resolution / 4, Resolution / 4];
for (int ti = 0, vi = 0, y = 0; y < Resolution; y++, vi++)
{
for (int x = 0; x < Resolution; x++, ti += 6, vi++)
{
triangles[ti] = vi;
triangles[ti + 3] = triangles[ti + 2] = vi + 1;
triangles[ti + 4] = triangles[ti + 1] = vi + Resolution + 1;
triangles[ti + 5] = vi + Resolution + 2;
Vector3[] cellVerts = new Vector3[]
{
vertices[vi], vertices[vi + 1], vertices[vi + Resolution + 1], vertices[vi + Resolution + 2]
};
Cell cell = new(new Vector2Int(x, y), cellVerts, CalculateCellGeometry(cellVerts));
cellMap[x / 4, y / 4] = cell;
}
}
mapMesh.triangles = triangles;
mapMesh.RecalculateNormals();
mapMesh.RecalculateTangents();
mapMesh.RecalculateBounds();
Map map = new(mapMesh, cellMap, heightFloatMap, vertices);
return map;
}
}
This works fine with grid sizes 16x16, 32x32... 256x256 but breaks when I try it on 512x512 or above
256x256
Mesh is perfect
512x512
It successfully triangulates up until the rows starting y=128
On the underside of the terrain there are these bars
I've mapped out the vertices generated from 512x512 and above resolutions and they are all good so I'm 99% sure its down to the triangulation.
I'm new to procedural meshes and am stumped by this issue, any help would be greatly appreciated.
Turns out it wasn't triangulation, the vertex limit was being reached as my mesh was set to use a 16-bit index buffer.
I added this line
mapMesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;
and the issue is fixed. An annoying oversight on my part but that's part of the learning process!

Translation matrix returns always 0

I'm working on 3d engine. But I have this problem with my translation matrix: it always returns 0, so on screen appears only one dot in the middle. I thought, that something was multiplying by 0. That wasn't the case. I also discovered, that some of the coords are extremely large. I need help.
Here is loop, where I project the points onto screen:
public override void OnUpdate(float elapsed)
{
Matice4x4 m = new Matice4x4();
Matice4x4 matRotZ = new Matice4x4(), matRotX = new Matice4x4();
//rotace z
matRotZ.m[0, 0] = (float)Math.Cos(fTheta);
matRotZ.m[0, 1] = (float)Math.Sin(fTheta);
matRotZ.m[1, 0] = -(float)Math.Sin(fTheta);
matRotZ.m[1, 1] = (float)Math.Cos(fTheta);
matRotZ.m[2, 2] = 1;
matRotZ.m[3, 3] = 1;
// rotace x
matRotX.m[0, 0] = 1;
matRotX.m[0, 1] = (float)Math.Cos(fTheta * 0.5f);
matRotX.m[1, 0] = (float)Math.Sin(fTheta * 0.5f);
matRotX.m[1, 1] = -(float)Math.Sin(fTheta * 0.5f);
matRotX.m[2, 2] = (float)Math.Cos(fTheta * 0.5f);
matRotX.m[3, 3] = 1;
fTheta += 1.0f * elapsed;
Vector3 projekce = new Vector3(), translace = new Vector3(), triRotZ = new Vector3(), triRotXZ = new Vector3();
foreach (Mesh mesh in meshes)
{
for (int i = 0; i < mesh.Vrcholy.Length; i++)
{
//rotace
m.MaticeVysledek(mesh.Vrcholy[i], triRotZ, matRotZ);
m.MaticeVysledek(triRotZ, triRotXZ, matRotX);
//translace
translace = triRotXZ;
translace.Z += 10.0f;
m.MaticeVysledek(translace, projekce, m);
projekce.X += 1.0f;
projekce.Y += 1.0f;
projekce.X *= 0.5f * (float)sirka;
projekce.Y *= 0.5f * (float)vyska;
Draw((int)projekce.X, (int)projekce.Y, Pixel.Presets.Red);
projekce.X = 0;
projekce.Y = 0;
projekce.Z = 0;
}
}
}
And here is the translation matrix:
struct Matice4x4
{
public float[,] m = new float[4, 4];
public Matice4x4()
{
var HE = new HerniEngine();
m[0, 0] = HE.fPomerStran * HE.fFovRad;
m[1, 1] = HE.fFovRad;
m[2, 2] = HE.fFar / (HE.fFar - HE.fNear);
m[3, 2] = (-HE.fFar * HE.fNear) / (HE.fFar - HE.fNear);
m[2, 3] = 1.0f;
m[3, 3] = 0.0f;
}
public void MaticeVysledek(Vector3 i, Vector3 o, Matice4x4 m)
{
o.X = i.X * m.m[0, 0] + i.Y * m.m[1, 0] + i.Z * m.m[2, 0] + m.m[3, 0];
o.Y = i.X * m.m[0, 1] + i.Y * m.m[1, 1] + i.Z * m.m[2, 1] + m.m[3, 1];
o.Z = i.X * m.m[0, 2] + i.Y * m.m[1, 2] + i.Z * m.m[2, 2] + m.m[3, 2];
float w = i.X * m.m[0, 3] + i.Y * m.m[1, 3] + i.Z * m.m[2, 3] + m.m[3, 3];
if (w != 0.0f)
{
o.X /= w;
o.Y /= w;
o.Z /= w;
}
}
}

Why my Triangles starts overlapping when terrainSize too high? [duplicate]

This question already has an answer here:
Is there maximum number of meshes in Unity? [duplicate]
(1 answer)
Closed last year.
I have a weird problem. At the moment I am doing a selfmade Terrain generator, for now I am doing a "plane" generation only for the bottom area with flat surface. My problem is that when I set terrainSize too high the triangles starts overlapping ironically.
Here is a picture when i set terrainSize to 120:
Here is a picture when i set the terrainSize to 200:
At the size 200 looks like its overlapping twice, i found out that the max terrainSize for me is 114 at 115 it starts overlapping.
Here is my code, maybe you can find something out and help me and other on this platform:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(MeshFilter))]
public class MeshGeneratorSecond : MonoBehaviour
{
[SerializeField] private int terrainSize;
private Mesh myMesh;
private Vector3[] vertices;
private int[] triangles;
private int verticesInVertex = 5;
private int trianglesInVertex = 4;
void Start()
{
myMesh = new Mesh();
GetComponent<MeshFilter>().mesh = myMesh;
vertices = new Vector3[terrainSize * terrainSize * 5];
triangles = new int[terrainSize * terrainSize * 12];
StartCoroutine(CreateShape());
}
IEnumerator CreateShape()
{
int vertex = 0;
int triangle = 0;
for(int x = 0; x < terrainSize; x++)
{
for(int z = 0; z < terrainSize; z++)
{
vertices[vertex] = new Vector3(x, 0, z);
vertices[vertex + 1] = new Vector3(x, 0, z + 1);
vertices[vertex + 2] = new Vector3(x + 1, 0, z + 1);
vertices[vertex + 3] = new Vector3(x + 1, 0, z);
vertices[vertex + 4] = new Vector3(x + 0.5f, 0, z + 0.5f);
//First Triangle
triangles[triangle] = vertex;
triangles[triangle + 1] = vertex + 1;
triangles[triangle + 2] = vertex + 4;
//Second Triangle
triangles[triangle + 3] = vertex + 1;
triangles[triangle + 4] = vertex + 2;
triangles[triangle + 5] = vertex + 4;
//Third Triangle
triangles[triangle + 6] = vertex + 2;
triangles[triangle + 7] = vertex + 3;
triangles[triangle + 8] = vertex + 4;
//Fourth Triangle
triangles[triangle + 9] = vertex + 3;
triangles[triangle + 10] = vertex;
triangles[triangle + 11] = vertex + 4;
vertex += verticesInVertex;
triangle += trianglesInVertex * 3;
}
UpdateMesh();
yield return new WaitForSeconds(.1f);
}
}
private void UpdateMesh()
{
myMesh.Clear();
myMesh.vertices = vertices;
myMesh.triangles = triangles;
myMesh.RecalculateNormals();
}
public void OnDrawGizmos()
{
Gizmos.color = Color.red;
for(int i = 0; i < vertices.Length; i++)
{
Gizmos.DrawSphere(vertices[i], .1f);
}
}
}
Mesh buffers are 16 bit by default. See Mesh-indexFormat:
Index buffer can either be 16 bit (supports up to 65535 vertices in a mesh), or 32 bit (supports up to 4 billion vertices). Default index format is 16 bit, since that takes less memory and bandwidth.
Note! Ehen you want to change the 16-bit buffer to 32-bit buffer, you can change with this line of code:
mesh.indexFormat = UnityEngine.Rendering.IndexFormat.UInt32;

How to rotate figure from points(Hilbert curve)?

I am working on Hilbert curve, and I cant rotate the whole figure, just lower rectangle(look at the screenshots, on third and next steps I have a problem)
first 3 steps
I have a Figure class. I use it to store every figure and build next firuge from previous one. Here is my code
public class Fragment
{
public static int PADDING = 50;
public static float sideLength;
private readonly Pen crimsonPen = new Pen(Color.Crimson);
public List<PointF> pointsF = new List<PointF>();
public PointF[] points;
public Fragment(int step, Graphics graphics)
{
sideLength = Form1.sideLenght;
points = new PointF[(int)Math.Pow(4, step + 1)];
if (step.Equals(0))
{
points[0] = new PointF(PADDING, PADDING + sideLength);
points[1] = new PointF(PADDING, PADDING);
points[2] = new PointF(PADDING + sideLength, PADDING);
points[3] = new PointF(PADDING + sideLength, PADDING + sideLength);
graphics.DrawLines(crimsonPen, new[] { points[0], points[1], points[2], points[3] });
}
else
{
var frag = Form1.fragments[step - 1];
for (var i = 0; i < step; i++)
{
PointF tmpPoint;
// left lower #1
for (int j = frag.points.Length - 1; j >= 0; j--)
{
points[frag.points.Length - 1 - j] = frag.points[j];
points[frag.points.Length - 1 - j].Y += sideLength * 2 * (i + 1);
}
//rotate left lower #1
for (int b = 0; b < Math.Pow(4, step) - 1; b++)
{
tmpPoint = points[0];
for (int j = 0; j < frag.points.Length; j++)
{
if (j.Equals(frag.points.Length - 1))
{
points[j] = tmpPoint;
}
else
{
points[j] = points[j + 1];
}
}
}
// left upper #2
for (int j = 0; j < frag.points.Length; j++)
{
points[j + frag.points.Length] = frag.points[j];
}
// right upper #3
for (int j = 0; j < frag.points.Length; j++)
{
points[j + 2 * frag.points.Length] = points[j + frag.points.Length];
points[j + 2 * frag.points.Length].X += sideLength * 2 * (i + 1);
}
//right lower #4
for (int j = frag.points.Length - 1; j >= 0; j--)
{
points[3 * frag.points.Length + j] = points[2 * frag.points.Length + frag.points.Length - j - 1];
points[3 * frag.points.Length + j].Y += sideLength * 2 * (i + 1);
}
tmpPoint = points[3 * frag.points.Length];
//rotate right lower #4
for (int j = 0; j < frag.points.Length; j++)
{
if (j.Equals(frag.points.Length - 1))
{
points[4 * (frag.points.Length) - 1] = tmpPoint;
}
else
{
points[3 * frag.points.Length + j] = points[3 * frag.points.Length + j + 1];
}
}
}
graphics.DrawLines(crimsonPen, points);
}
}
}
Here I use my recursive method to draw figures
private void drawButton_Click(object sender, EventArgs e)
{
canvas.Refresh();
count = 0;
if (Int32.TryParse(stepsTextBox.Text, out steps))
{
sideLenght = (float)((canvas.Width - 100) / (Math.Pow(2, steps) - 1));
fragments = new Fragment[steps];
drawCurve();
}
else
{
MessageBox.Show("Wow, incorrect input", "Try again");
}
}
private void drawCurve()
{
if (count < steps)
{
fragments[count] = new Fragment(count, graphics);
++count;
drawCurve();
}
}
I've tried to rotate points around figure center and use next code but the rotation is incorrect
public PointF rotatePoint(PointF pointToRotate)
{
pointToRotate.X = (float)(Math.Cos(180 * Math.PI / 180) * (pointToRotate.X - centerPoint.X) -
Math.Sin(180 * Math.PI / 180) * (pointToRotate.Y - centerPoint.Y) +
centerPoint.X);
pointToRotate.Y = (float)(Math.Sin(0 * Math.PI / 180) * (pointToRotate.X - centerPoint.X) +
Math.Cos(0 * Math.PI / 180) * (pointToRotate.Y - centerPoint.Y) +
centerPoint.Y);
return pointToRotate;
}
The problem is that you are using the X co-ordinate that you have already rotated when you calculate the rotated Y co-ordinate. Use a temp variable to avoid this:
public PointF rotatePoint(PointF pointToRotate)
{
float rotatedX = (float)(Math.Cos(180 * Math.PI / 180) * (pointToRotate.X - centerPoint.X) -
Math.Sin(180 * Math.PI / 180) * (pointToRotate.Y - centerPoint.Y) +
centerPoint.X);
pointToRotate.Y = (float)(Math.Sin(0 * Math.PI / 180) * (pointToRotate.X - centerPoint.X) +
Math.Cos(0 * Math.PI / 180) * (pointToRotate.Y - centerPoint.Y) +
centerPoint.Y);
pointToRotate.X = rotatedX;
return pointToRotate;
}

Procedural Terrain Generation Spiking Error

I'm getting a problem with my terrain manager where I get these weirdly spiked columns instead of normal terrain (if there is a normal with this kind of generation) any ideas?
Image : http://bit.ly/1IVVsET
Code :
public class TerrainManager : MonoBehaviour, ICameraObserver {
public Terrain mainTerrain;
public float terrainChangeRate = 0.0001f;
public int brushArea = 20;
public static int viewDistance = 9;
public static Vector2 VECTOR_WILDCARD = new Vector2(-10000, -10000);
int resolutionX;
int resolutionY;
float[,] heights;
int heightEdit = 1;
//Chunks
List<Vector2> loadedChunks = new List<Vector2>();
Vector2[] visibleChunks = null;
Terrain[] chunkGraphics = new Terrain[viewDistance];
Vector2 curChunkIndex = new Vector2();
int chunkSizeX = 256;
int chunkSizeY = 256;
// Use this for initialization
void Start () {
resolutionX = mainTerrain.terrainData.heightmapWidth;
resolutionY = mainTerrain.terrainData.heightmapHeight;
heights = mainTerrain.terrainData.GetHeights(0, 0, resolutionX, resolutionY);
Camera.main.GetComponent<RTSCamera>().Subscribe(this);
GameObject world = new GameObject();
world.name = "World";
for (int i = 0; i < viewDistance; i++)
{
GameObject go = new GameObject();
go.name = "Chunk_" + i;
go.transform.SetParent(world.transform);
chunkGraphics[i] = go.AddComponent<Terrain>();
chunkGraphics[i].terrainData = new TerrainData();
go.AddComponent<TerrainCollider>().terrainData = chunkGraphics[i].terrainData;
chunkGraphics[i].terrainData.size = new Vector3((int)(chunkSizeX / 4), 600, (int)(chunkSizeY / 4));
chunkGraphics[i].terrainData.heightmapResolution = (int)(chunkSizeX / 2);
}
onCameraMove(Camera.main.transform.position);
}
// Update is called once per frame
void Update () {
if (Input.GetMouseButton(0))
{
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
editTerrainHeight(hit.point, terrainChangeRate, brushArea);
}
}
}
void editTerrainHeight(Vector3 position, float amount, int diameter)
{
int terrainPosX = (int)((position.x / mainTerrain.terrainData.size.x) * resolutionX);
int terrainPosY = (int)((position.z / mainTerrain.terrainData.size.z) * resolutionY);
float[,] heightChange = new float[diameter, diameter];
int radius = diameter / 2;
if (Input.GetKey(KeyCode.LeftShift))
{
heightEdit = -1;
}
else
{
heightEdit = 1;
}
amount = amount * heightEdit;
for (int x = 0; x < diameter; x++)
{
for (int y = 0; y < diameter; y++)
{
int x2 = x - radius;
int y2 = y - radius;
heightChange[y, x] = heights[terrainPosY + y2, terrainPosX + x2] + amount;
heights[terrainPosY + y2, terrainPosX + x2] = heightChange[y, x];
}
}
mainTerrain.terrainData.SetHeights(terrainPosX - radius, terrainPosY - radius, heightChange);
}
public void onCameraMove(Vector3 newCameraPosition)
{
int chunkIndexX = Mathf.FloorToInt(newCameraPosition.x / chunkSizeX);
int chunkIndexY = Mathf.FloorToInt(newCameraPosition.z / chunkSizeY);
if (curChunkIndex.x == chunkIndexX && curChunkIndex.y == chunkIndexY)
{
return;
}
curChunkIndex.x = chunkIndexX;
curChunkIndex.y = chunkIndexY;
Vector2[] newVisibleChunks = new Vector2[viewDistance];
newVisibleChunks[0] = new Vector2(chunkIndexX - 1, chunkIndexY +1);
newVisibleChunks[1] = new Vector2(chunkIndexX, chunkIndexY +1);
newVisibleChunks[2] = new Vector2(chunkIndexX + 1, chunkIndexY +1);
newVisibleChunks[3] = new Vector2(chunkIndexX -1, chunkIndexY);
newVisibleChunks[4] = new Vector2(chunkIndexX, chunkIndexY);
newVisibleChunks[5] = new Vector2(chunkIndexX + 1, chunkIndexY);
newVisibleChunks[6] = new Vector2(chunkIndexX - 1, chunkIndexY -1);
newVisibleChunks[7] = new Vector2(chunkIndexX, chunkIndexY -1);
newVisibleChunks[8] = new Vector2(chunkIndexX + 1, chunkIndexY -1);
Terrain[] newChunkGraphics = new Terrain[viewDistance];
List<int> freeTerrains = new List<int>();
List<int> loadingIndexes = new List<int>();
for (int i = 0; i < viewDistance; i++)
{
bool found = false;
for (int j = 0; j < viewDistance; j++)
{
if (visibleChunks == null)
{
break;
}
if (newVisibleChunks[i].Equals(visibleChunks[j]))
{
visibleChunks[j] = VECTOR_WILDCARD;
newChunkGraphics[i] = chunkGraphics[j];
found = true;
break;
}
}
if (!found)
{
loadingIndexes.Add(i);
}
}
if (visibleChunks != null)
{
for (int i = 0; i < viewDistance; i++)
{
if (visibleChunks[i] != VECTOR_WILDCARD)
{
freeTerrains.Add(i);
saveChunkToMemory(chunkGraphics[i], visibleChunks[i]);
}
}
}
else
{
for (int i = 0; i < viewDistance; i++)
{
freeTerrains.Add(i);
}
}
for (int i = 0; i < loadingIndexes.Count; i++)
{
loadChunkFromMemory(newVisibleChunks[loadingIndexes[i]], freeTerrains[i]);
newChunkGraphics[loadingIndexes[i]] = chunkGraphics[freeTerrains[i]];
}
visibleChunks = newVisibleChunks;
chunkGraphics = newChunkGraphics;
}
void loadChunkFromMemory(Vector2 cordIndex, int graphicIndex)
{
bool found = false;
foreach (Vector2 v in loadedChunks)
{
if (v == cordIndex)
{
found = true;
break;
}
}
GameObject terrainGO;
if (!found)
{
terrainGO = generateChunk(cordIndex, graphicIndex);
}
else
{
//Load Chunk from Memory
Debug.Log("Loading Chunk(" + cordIndex.x + "," + cordIndex.y + ")");
terrainGO = chunkGraphics[graphicIndex].gameObject;
}
terrainGO.transform.position = new Vector3(chunkSizeX * cordIndex.x, 0, chunkSizeY * cordIndex.y);
}
GameObject generateChunk(Vector2 cordIndex, int graphicIndex)
{
GameObject terrainGO = chunkGraphics[graphicIndex].gameObject;
loadedChunks.Add(cordIndex);
setTerrainHeightMap(terrainGO.GetComponent<Terrain>(), cordIndex);
return terrainGO;
}
void setTerrainHeightMap(Terrain terrain, Vector2 cordIndex)
{
float[,] heights = new float[terrain.terrainData.heightmapHeight, terrain.terrainData.heightmapWidth];
heights[0, 0] = 0.5f;
heights[terrain.terrainData.heightmapWidth - 1, 0] = 0.5f;
heights[0, terrain.terrainData.heightmapHeight - 1] = 0.5f;
heights[terrain.terrainData.heightmapWidth - 1, terrain.terrainData.heightmapHeight - 1] = 0.5f;
heights = diamondSquare(heights, 0, 0, terrain.terrainData.heightmapWidth - 1, 0);
terrain.terrainData.SetHeights(0, 0, heights);
}
float[,] getTerrainHeightMap(Vector2 cordIndex)
{
return null;
}
float[,] diamondSquare(float[,] heights, int offSetX, int offSetY, int squareSize, int depth)
{
if (squareSize == 1)
{
return heights;
}
float topLeft = heights[offSetY, offSetX];
float topRight = heights[offSetY, offSetX + squareSize];
float bottomLeft = heights[offSetY + squareSize, offSetX];
float bottomRight = heights[offSetY + squareSize, offSetX + squareSize];
int size = squareSize / 2;
if (topLeft == 0 || topRight == 0 || bottomLeft == 0 || bottomRight == 0)
{
Debug.LogError("One or more Corner Seeds have not been set..");
}
if (heights[offSetY + size, offSetX + size] == 0)
{
heights[offSetY + size, offSetX + size] = getRandomHeight(depth + (int)averagePoints(topLeft, topRight, bottomLeft, bottomRight));
}
float centrePoint = heights[offSetY + size, offSetX + size];
//left Diamond
float runningAverage = averagePoints(topLeft, centrePoint, bottomLeft);
if (offSetX - size > 0 && heights[offSetY + size, offSetX - size] != 0)
{
runningAverage = averagePoints(topLeft, centrePoint, bottomLeft, heights[offSetY + size, offSetX - size]);
}
if (heights[offSetY + size, offSetX] == 0)
{
heights[offSetY + size, offSetX] = runningAverage + getRandomHeight(depth);
}
//right Diamond
runningAverage = averagePoints(topRight, centrePoint, bottomRight);
if (offSetX + (squareSize * 1.5f) < heights.GetLength(1) && heights[offSetY + size, offSetX + (int)(squareSize * 1.5f)] != 0)
{
runningAverage = averagePoints(topRight, centrePoint, bottomRight, heights[offSetY + size, offSetX + (int)(squareSize * 1.5f)]);
}
if (heights[offSetY + size, offSetX + squareSize] == 0)
{
heights[offSetY + size, offSetX + squareSize] = runningAverage + getRandomHeight(depth);
}
//top Diamond
runningAverage = averagePoints(topLeft, centrePoint, topRight);
if (offSetY - size > 0 && heights[offSetY - size, offSetX + size] != 0)
{
runningAverage = averagePoints(topLeft, centrePoint, topRight, heights[offSetY - size, offSetX + size]);
}
if (heights[offSetY, offSetX + size] == 0)
{
heights[offSetY, offSetX + size] = runningAverage + getRandomHeight(depth);
}
//bottom Diamond
runningAverage = averagePoints(bottomRight, centrePoint, bottomLeft);
if (offSetY + (squareSize * 1.5f) < heights.GetLength(0) && heights[offSetY + (int)(squareSize * 1.5f), offSetX + size] != 0)
{
runningAverage = averagePoints(bottomRight, centrePoint, topRight, heights[offSetY + (int)(squareSize * 1.5f), offSetX + size]);
}
if (heights[offSetY + squareSize, offSetX + size] == 0)
{
heights[offSetY + squareSize, offSetX + size] = runningAverage + getRandomHeight(depth);
}
heights = diamondSquare(heights, offSetX, offSetY, size, depth + 1);
heights = diamondSquare(heights, offSetX + size, offSetY, size, depth + 1);
heights = diamondSquare(heights, offSetX, offSetY + size, size, depth + 1);
heights = diamondSquare(heights, offSetX + size, offSetY + size, size, depth + 1);
return heights;
}
float averagePoints(float p1, float p2, float p3, float p4)
{
return (p1 + p2 + p3 + p4) * 0.25f;
}
float averagePoints(float p1, float p2, float p3)
{
return (p1 + p2 + p3) * 0.3333f;
}
float getRandomHeight(int depth)
{
return Random.Range(-0.1f, 0.0f) / Mathf.Pow(2, depth);
}
void saveChunkToMemory(Terrain chunk, Vector2 index)
{
Debug.Log("Unloading Chunk(" + index.x + "," + index.y + ")");
}
}
So after a few more hours here is a fix :D
public class TerrainManager : MonoBehaviour, ICameraObserver {
public Terrain mainTerrain;
public float terrainChangeRate = 0.0001f;
public int brushArea = 20;
public static int viewDistance = 9;
public static Vector2 VECTOR_WILDCARD = new Vector2(-10000, -10000);
int resolutionX;
int resolutionY;
float[,] heights;
int heightEdit = 1;
//Chunks
List<Vector2> loadedChunks = new List<Vector2>();
Vector2[] visibleChunks = null;
Terrain[] chunkGraphics = new Terrain[viewDistance];
Vector2 curChunkIndex = new Vector2();
int chunkSizeX = 256;
int chunkSizeY = 256;
// Use this for initialization
void Start () {
resolutionX = mainTerrain.terrainData.heightmapWidth;
resolutionY = mainTerrain.terrainData.heightmapHeight;
heights = mainTerrain.terrainData.GetHeights(0, 0, resolutionX, resolutionY);
Camera.main.GetComponent<RTSCamera>().Subscribe(this);
GameObject world = new GameObject();
world.name = "World";
for (int i = 0; i < viewDistance; i++)
{
GameObject go = new GameObject();
go.name = "Chunk_" + i;
go.transform.SetParent(world.transform);
chunkGraphics[i] = go.AddComponent<Terrain>();
chunkGraphics[i].terrainData = new TerrainData();
go.AddComponent<TerrainCollider>().terrainData = chunkGraphics[i].terrainData;
chunkGraphics[i].terrainData.size = new Vector3((int)(chunkSizeX / 4), 600, (int)(chunkSizeY / 4));
chunkGraphics[i].terrainData.heightmapResolution = (int)(chunkSizeX / 2);
}
onCameraMove(Camera.main.transform.position);
}
// Update is called once per frame
void Update () {
if (Input.GetMouseButton(0))
{
RaycastHit hit;
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out hit))
{
editTerrainHeight(hit.point, terrainChangeRate, brushArea);
}
}
}
void editTerrainHeight(Vector3 position, float amount, int diameter)
{
int terrainPosX = (int)((position.x / mainTerrain.terrainData.size.x) * resolutionX);
int terrainPosY = (int)((position.z / mainTerrain.terrainData.size.z) * resolutionY);
float[,] heightChange = new float[diameter, diameter];
int radius = diameter / 2;
if (Input.GetKey(KeyCode.LeftShift))
{
heightEdit = -1;
}
else
{
heightEdit = 1;
}
amount = amount * heightEdit;
for (int x = 0; x < diameter; x++)
{
for (int y = 0; y < diameter; y++)
{
int x2 = x - radius;
int y2 = y - radius;
heightChange[y, x] = heights[terrainPosY + y2, terrainPosX + x2] + amount;
heights[terrainPosY + y2, terrainPosX + x2] = heightChange[y, x];
}
}
mainTerrain.terrainData.SetHeights(terrainPosX - radius, terrainPosY - radius, heightChange);
}
public void onCameraMove(Vector3 newCameraPosition)
{
//Debug.Log ("Camera Moved");
int chunkIndexX = Mathf.FloorToInt (newCameraPosition.x / chunkSizeX);
int chunkIndexY = Mathf.FloorToInt (newCameraPosition.z / chunkSizeY);
if (curChunkIndex.x == chunkIndexX && curChunkIndex.y == chunkIndexY) {
return;
}
curChunkIndex.x = chunkIndexX;
curChunkIndex.y = chunkIndexY;
//Debug.Log ("Chunk Index ( " + chunkIndexX + ", " + chunkIndexY + " )");
Vector2[] newVisibleChunks = new Vector2[9];
newVisibleChunks [0] = new Vector2 (chunkIndexX -1, chunkIndexY +1);
newVisibleChunks [1] = new Vector2 (chunkIndexX, chunkIndexY +1);
newVisibleChunks [2] = new Vector2 (chunkIndexX +1, chunkIndexY +1);
newVisibleChunks [3] = new Vector2 (chunkIndexX -1, chunkIndexY);
newVisibleChunks [4] = new Vector2 (chunkIndexX, chunkIndexY);
newVisibleChunks [5] = new Vector2 (chunkIndexX +1, chunkIndexY);
newVisibleChunks [6] = new Vector2 (chunkIndexX -1, chunkIndexY -1);
newVisibleChunks [7] = new Vector2 (chunkIndexX, chunkIndexY -1);
newVisibleChunks [8] = new Vector2 (chunkIndexX +1, chunkIndexY -1);
Terrain[] newChunkGraphics = new Terrain[chunkGraphics.Length];
List<int> freeTerrains = new List<int>();
List<int> loadingIndexes = new List<int>();
//List<int> newIndex = new List<int>();
for (int i =0; i <9; i++) {
bool found = false;
for (int j =0; j <9; j++) {
if (visibleChunks == null)
break;
if (newVisibleChunks[i].Equals (visibleChunks[j])){
visibleChunks[j] = VECTOR_WILDCARD;
newChunkGraphics[i] = chunkGraphics[j];
found = true;
break;
}
}
if (!found){
loadingIndexes.Add (i);
}
}
if (visibleChunks != null) {
for (int i = 0; i< 9; i++) {
if (visibleChunks [i] != VECTOR_WILDCARD) {
freeTerrains.Add (i);
saveChunkToMemory (chunkGraphics [i], visibleChunks [i]);
}
}
} else {
for (int i = 0; i< 9; i++) {
freeTerrains.Add (i);
}
}
for (int i = 0; i < loadingIndexes.Count; i++) {
newChunkGraphics[loadingIndexes[i]] = chunkGraphics[freeTerrains[i]];
}
visibleChunks = newVisibleChunks;
chunkGraphics = newChunkGraphics;
for (int i = 0; i < loadingIndexes.Count; i++) {
loadChunkFromMemory(visibleChunks[loadingIndexes[i]], loadingIndexes[i]);
}
}
void loadChunkFromMemory(Vector2 cordIndex, int graphicIndex)
{
bool found = false;
foreach (Vector2 v in loadedChunks)
{
if (v == cordIndex)
{
found = true;
break;
}
}
GameObject terrainGO;
if (!found)
{
terrainGO = generateChunk(cordIndex, graphicIndex);
}
else
{
//Load Chunk from Memory
Debug.Log("Loading Chunk(" + cordIndex.x + "," + cordIndex.y + ")");
terrainGO = chunkGraphics[graphicIndex].gameObject;
}
terrainGO.transform.position = new Vector3(chunkSizeX * cordIndex.x, 0, chunkSizeY * cordIndex.y);
}
GameObject generateChunk(Vector2 cordIndex, int graphicIndex)
{
GameObject terrainGO = chunkGraphics[graphicIndex].gameObject;
loadedChunks.Add(cordIndex);
setTerrainHeightMap(terrainGO.GetComponent<Terrain>(), cordIndex);
return terrainGO;
}
void setTerrainHeightMap(Terrain terrain, Vector2 cordIndex)
{
float[,] heights = new float[terrain.terrainData.heightmapHeight, terrain.terrainData.heightmapWidth];
bool left = false;
bool right = false;
bool top = false;
bool bottom = false;
//left
float[,] hm = getTerrainHeightMap(new Vector2(cordIndex.x - 1, cordIndex.y));
if(hm != null)
{
left = true;
for (int i = 0; i < hm.GetLength(0); i++)
{
heights[i, 0] = hm[i, hm.GetLength(1) - 1];
}
}
//Right
hm = getTerrainHeightMap(new Vector2(cordIndex.x + 1, cordIndex.y));
if (hm != null)
{
right = true;
for (int i = 0; i < hm.GetLength(0); i++)
{
heights[i, heights.GetLength(1) - 1] = hm[i, 0];
}
}
hm = getTerrainHeightMap(new Vector2(cordIndex.x, cordIndex.y - 1));
if (hm != null)
{
top = true;
for (int i = 0; i < hm.GetLength(1); i++)
{
heights[i, 0] = hm[hm.GetLength(0) - 1, i];
}
}
hm = getTerrainHeightMap(new Vector2(cordIndex.x, cordIndex.y + 1));
if (hm != null)
{
bottom = true;
for (int i = 0; i < hm.GetLength(0); i++)
{
heights[heights.GetLength(1) - 1, i] = hm [0, i];
}
}
if (!top && !left)
{
heights[0, 0] = 0.2f;
}
if (!bottom && !left)
{
heights[terrain.terrainData.heightmapHeight - 1, 0] = 0.2f;
}
if (!top && !right)
{
heights[0, terrain.terrainData.heightmapWidth - 1] = 0.2f;
}
if (!bottom && !right)
{
heights[terrain.terrainData.heightmapHeight - 1, terrain.terrainData.heightmapWidth - 1] = 0.2f;
}
heights[0, 0] = 0.2f;
heights[terrain.terrainData.heightmapHeight - 1, 0] = 0.2f;
heights[0, terrain.terrainData.heightmapWidth - 1] = 0.2f;
heights[terrain.terrainData.heightmapHeight - 1, terrain.terrainData.heightmapWidth - 1] = 0.2f;
heights = diamondSquare(heights, 0, 0, terrain.terrainData.heightmapWidth - 1, 0);
terrain.terrainData.SetHeights(0, 0, heights);
}
float[,] getTerrainHeightMap(Vector2 cordIndex)
{
if (loadedChunks.Contains(cordIndex))
{
for (int i = 0; i < visibleChunks.Length; i++)
{
if (visibleChunks[i].x == cordIndex.x && visibleChunks[i].y == cordIndex.y)
{
return chunkGraphics[i].terrainData.GetHeights(0, 0, chunkGraphics[i].terrainData.heightmapWidth, chunkGraphics[i].terrainData.heightmapWidth);
}
}
return loadHeightMapFromMemory(cordIndex);
}
else
{
return null;
}
}
float[,] loadHeightMapFromMemory(Vector2 cordIndex)
{
return null;
}
float[,] diamondSquare(float[,] heights, int offSetX, int offSetY, int squareSize, int depth)
{
if (squareSize == 1)
return heights;
float topLeft = heights[offSetY, offSetX];
float topRight = heights[offSetY, offSetX + squareSize];
float bottomLeft = heights[offSetY + squareSize, offSetX];
float bottomRight = heights[offSetY + squareSize, offSetX + squareSize];
if (topLeft == 0 || topRight == 0 || bottomLeft == 0 || bottomRight == 0)
{
Debug.LogError("One or more Corner Seed Values is not set");
}
if (heights[offSetY + (squareSize / 2), offSetX + (squareSize / 2)] == 0)
{
heights[offSetY + (squareSize / 2), offSetX + (squareSize / 2)] = getRandomHeight(depth) + averagePoints(topLeft, topRight, bottomLeft, bottomRight);
}
float centrePoint = heights[offSetY + (squareSize / 2), offSetX + (squareSize / 2)];
//left diamond
float runningAverage = averagePoints(topLeft, centrePoint, bottomLeft);
if (offSetX - (squareSize / 2) > 0 && heights[offSetY + (squareSize / 2), offSetX - (squareSize / 2)] != 0)
{
runningAverage = averagePoints(topLeft, centrePoint, bottomLeft, heights[offSetY + (squareSize / 2), offSetX - (squareSize / 2)]);
}
if (heights[offSetY + (squareSize / 2), offSetX] == 0)
{
heights[offSetY + (squareSize / 2), offSetX] = runningAverage + getRandomHeight(depth);
}
//right diamond
runningAverage = averagePoints(topRight, centrePoint, bottomRight);
if (offSetX + (squareSize * 1.5f) < heights.GetLength(1) && heights[offSetY + (squareSize / 2), offSetX + (int)(squareSize * 1.5f)] != 0)
{
runningAverage = averagePoints(topRight, centrePoint, bottomRight, heights[offSetY + (squareSize / 2), offSetX + (int)(squareSize * 1.5f)]);
}
if (heights[offSetY + (squareSize / 2), offSetX + squareSize] == 0)
{
heights[offSetY + (squareSize / 2), offSetX + squareSize] = runningAverage + getRandomHeight(depth);
}
//top diamond
runningAverage = averagePoints(topLeft, centrePoint, topRight);
if (offSetY - (squareSize / 2) > 0 && heights[offSetY - (squareSize / 2), offSetX + (squareSize / 2)] != 0)
{
runningAverage = averagePoints(topLeft, centrePoint, topRight, heights[offSetY - (squareSize / 2), offSetX + (squareSize / 2)]);
}
if (heights[offSetY, offSetX + (squareSize / 2)] == 0)
{
heights[offSetY, offSetX + (squareSize / 2)] = runningAverage + getRandomHeight(depth);
}
//bottom diamond
runningAverage = averagePoints(bottomRight, centrePoint, bottomLeft);
if (offSetY + (int)(squareSize * 1.5f) < heights.GetLength(0) && heights[offSetY + (int)(squareSize * 1.5f), offSetX + (squareSize / 2)] != 0)
{
runningAverage = averagePoints(bottomRight, centrePoint, topRight, heights[offSetY + (int)(squareSize * 1.5f), offSetX + (squareSize / 2)]);
}
if (heights[offSetY + squareSize, offSetX + (squareSize / 2)] == 0)
{
heights[offSetY + squareSize, offSetX + (squareSize / 2)] = runningAverage + getRandomHeight(depth);
}
heights = diamondSquare(heights, offSetX, offSetY, squareSize / 2, depth + 1);//top left
heights = diamondSquare(heights, offSetX + (squareSize / 2), offSetY, squareSize / 2, depth + 1);//top right
heights = diamondSquare(heights, offSetX, offSetY + (squareSize / 2), squareSize / 2, depth + 1);//bottom left
heights = diamondSquare(heights, offSetX + (squareSize / 2), offSetY + (squareSize / 2), squareSize / 2, depth + 1);//bottom right'
return heights;
}
float averagePoints(float p1, float p2, float p3, float p4)
{
return (p1 + p2 + p3 + p4) / 4;
}
float averagePoints(float p1, float p2, float p3)
{
return (p1 + p2 + p3) / 3;
}
float getRandomHeight(int depth)
{
return Random.Range(-0.1f, 0.1f) / Mathf.Pow(2, depth);
}
void saveChunkToMemory(Terrain chunk, Vector2 index)
{
Debug.Log("Unloading Chunk(" + index.x + "," + index.y + ")");
}

Categories