1 module isodi.chunk;
2 
3 import raylib;
4 
5 import std.meta;
6 import std.array;
7 import std.range;
8 import std.format;
9 import std.algorithm;
10 
11 import isodi.utils;
12 import isodi.properties;
13 import isodi.isodi_model;
14 
15 
16 @safe:
17 
18 
19 /// Represents a chunk of blocks.
20 struct Chunk {
21 
22     public {
23 
24         /// Properties of the chunk.
25         Properties properties;
26 
27         /// Seed to use to generate variants on this chunk.
28         ulong seed;
29 
30         /// Mapping of block types to their position in the texture.
31         BlockUV[BlockType] atlas;
32 
33         /// Blocks making up the chunk.
34         Block[Vector2I] blocks;
35 
36     }
37 
38     inout(Block*) find(Vector2I position) inout {
39 
40         return position in blocks;
41 
42     }
43 
44     /// Add multiple items.
45     ///
46     /// * Pass a `BlockPosition` to change the position of the following blocks.
47     /// * Pass a `BlockType` to change the type of the following blocks.
48     /// * Pass a `long` to add a new block with given height. Increment X or Y of the block position, respectively for
49     ///   the function used.
50     void addX(T...)(T items) => addImpl!"x"(items);
51 
52     /// ditto
53     void addY(T...)(T items) => addImpl!"y"(items);
54 
55     void addImpl(string direction, T...)(T items) {
56 
57         BlockPosition position;
58         BlockType type;
59 
60         foreach (item; items) {
61 
62             alias T = typeof(item);
63 
64             // Changing position
65             static if (is(T : BlockPosition)) {
66 
67                 position = item;
68 
69             }
70 
71             // Changing type
72             else static if (is(T : BlockType)) {
73 
74                 type = item;
75 
76             }
77 
78             // Adding block
79             else static if (is(T : long)) {
80 
81                 auto blockPosition = position;
82                 blockPosition.height = item;
83 
84                 blocks[position.vector] = Block(type, blockPosition);
85 
86                 // Update the position
87                 __traits(getMember, position, direction) += 1;
88 
89             }
90 
91             else static assert(false, format!"Unrecognized type %s"(typeid(T)));
92 
93         }
94 
95     }
96 
97     /// Make model for the chunk.
98     IsodiModel makeModel(return Texture2D texture) const {
99 
100         import core.lifetime;
101 
102         const atlasSize = Vector2(texture.width, texture.height);
103 
104         // Render data per block
105         const verticesPerBlock = 5*4;
106         const trianglesPerBlock = 5*2;
107 
108         // Total data
109         const vertexCount = blocks.length * verticesPerBlock;
110         const triangleCount = blocks.length * trianglesPerBlock;
111 
112         // Prepare the model
113         IsodiModel model = {
114             properties: properties,
115             texture: texture,
116             performFold: true,
117         };
118         model.vertices.reserve = vertexCount;
119         model.variants.length = vertexCount;
120         model.texcoords.reserve = vertexCount;
121         model.triangles.reserve = triangleCount;
122         // TODO: side culling
123 
124         // Add each block
125         foreach (i, block; blocks.byValue.enumerate) with (model) {
126 
127             const position = Vector3(
128                 block.position.x,
129                 cast(float) block.position.height / properties.heightSteps,
130                 block.position.y,
131             );
132 
133             const depth = cast(float) block.position.depth / properties.heightSteps;
134 
135             // Vertices
136             vertices ~= [
137 
138                 // Tile
139                 position + Vector3(-0.5, 0, 0.5),
140                 position + Vector3(0.5, 0, 0.5),
141                 position + Vector3(0.5, 0, -0.5),
142                 position + Vector3(-0.5, 0, -0.5),
143 
144                 // North side (negative Z)
145                 position + Vector3(0.5, -depth, -0.5),
146                 position + Vector3(-0.5, -depth, -0.5),
147                 position + Vector3(-0.5, 0, -0.5),
148                 position + Vector3(0.5, 0, -0.5),
149 
150                 // East side (positive X)
151                 position + Vector3(0.5, -depth, 0.5),
152                 position + Vector3(0.5, -depth, -0.5),
153                 position + Vector3(0.5, 0, -0.5),
154                 position + Vector3(0.5, 0, 0.5),
155 
156                 // South side (positive Z)
157                 position + Vector3(-0.5, -depth, 0.5),
158                 position + Vector3(0.5, -depth, 0.5),
159                 position + Vector3(0.5, 0, 0.5),
160                 position + Vector3(-0.5, 0, 0.5),
161 
162                 // West side (negative X)
163                 position + Vector3(-0.5, -depth, -0.5),
164                 position + Vector3(-0.5, -depth, 0.5),
165                 position + Vector3(-0.5, 0, 0.5),
166                 position + Vector3(-0.5, 0, -0.5),
167 
168             ];
169 
170             // UVs — tile
171             texcoords ~= [
172                 Vector2(0, 1),
173                 Vector2(1, 1),
174                 Vector2(1, 0),
175                 Vector2(0, 0),
176             ];
177 
178             // UVs — sides
179             foreach (j; 1..5) texcoords ~= [
180                 Vector2(0, depth),
181                 Vector2(1, depth),
182                 Vector2(1, 0),
183                 Vector2(0, 0),
184             ];
185 
186             const chunkIndex = i * trianglesPerBlock/2;
187 
188             // Get the variants
189             const blockUV = block.type in atlas;
190             assert(blockUV, format!"%s is not present in chunk atlas"(block.type));
191 
192             // Tile variant
193             const tileVariant = blockUV.getTile(block.position.vector, seed).toShader(atlasSize);
194             variants.assign(chunkIndex + 0, 4, tileVariant);
195 
196             // Side variant
197             foreach (j; 1..5) {
198 
199                 const sideVariant = blockUV.getSide(block.position.vector, seed+j).toShader(atlasSize);
200                 variants.assign(chunkIndex + j, 4, sideVariant);
201 
202             }
203 
204             ushort[3] value(int[] offsets) => [
205                 cast(ushort) (i*verticesPerBlock + offsets[0]),
206                 cast(ushort) (i*verticesPerBlock + offsets[1]),
207                 cast(ushort) (i*verticesPerBlock + offsets[2]),
208             ];
209 
210             // Triangles (2 per rectangle)
211             triangles ~= map!value([
212                 [ 0,  1,  2],  [ 0,  2,  3],
213                 [ 4,  5,  6],  [ 4,  6,  7],
214                 [ 8,  9, 10],  [ 8, 10, 11],
215                 [12, 13, 14],  [12, 14, 15],
216                 [16, 17, 18],  [16, 18, 19],
217             ]).array;
218 
219         }
220 
221         // Upload the model
222         model.upload();
223 
224         // Return it
225         return model;
226 
227     }
228 
229 }
230 
231 struct BlockPosition {
232 
233     int x, y;
234     int height, depth;
235 
236     Vector2I vector() @nogc const => Vector2I(x, y);
237     Vector2 vectorf() @nogc const => Vector2(x, y);
238 
239 }
240 
241 struct Block {
242 
243     BlockType type;
244     BlockPosition position;
245 
246 }
247 
248 struct BlockType {
249 
250     /// Global user-defined block ID.
251     ulong typeID;
252 
253 }
254 
255 /// Texture position data for given block.
256 struct BlockUV {
257 
258     RectangleI tileArea;
259     RectangleI sideArea;
260     uint tileSize;
261     uint sideSize;
262 
263     /// Get random tile variant within the UV.
264     RectangleI getTile(Vector2I position, ulong seed) @nogc @trusted const
265     in (tileArea.width > 0, "Tile area width must be positive")
266     in (tileArea.height > 0, "Tile area height must be positive")
267     do {
268 
269         // Get tile variant to use
270         const variant = randomVariant(
271             Vector2I(tileArea.width, tileArea.height),
272             Vector2I(tileSize, tileSize),
273             seed + position.toHash,
274         );
275 
276         return RectangleI(
277             tileArea.x + variant.x,
278             tileArea.y + variant.y,
279             tileSize,
280             tileSize,
281         );
282 
283     }
284 
285     /// Get random side variant within the UV.
286     RectangleI getSide(Vector2I position, ulong seed) @nogc @trusted const {
287 
288         // Get tile variant to use
289         const variant = randomVariant(
290             Vector2I(sideArea.width, sideArea.height),
291             Vector2I(tileSize, sideSize),
292             seed + position.toHash,
293         );
294 
295         return RectangleI(
296             sideArea.x + variant.x,
297             sideArea.y + variant.y,
298             tileSize,
299             sideSize,
300         );
301 
302     }
303 
304 }