1 module isodi.isodi_model;
2 
3 import raylib;
4 import std.format;
5 
6 import isodi.utils;
7 import isodi.properties;
8 
9 
10 @safe:
11 
12 
13 /// This struct represents an Isodi model uploaded to the GPU. Its resources are managed by Isodi and must NOT be
14 /// cleaned up with Raylib functions, with the exception of the texture.
15 struct IsodiModel {
16 
17     public {
18 
19         /// Rendering properties of the model.
20         Properties properties;
21 
22         /// Atlas texture to be used by the model. Will NOT be freed along with the model.
23         Texture2D texture;
24 
25         /// Texture used to send matrices to the model.
26         ///
27         /// The expected texture format is R32G32B32A32 (each pixel is a single matrix column). The image is to be 4
28         /// pixels wide and as high as the model's bone count.
29         ///
30         /// Requires setting `bones` for each vertex.
31         Texture2D matrixTexture;
32         // TODO embed variant data in this texture as well. Using two booleans we could control if matrices are to be
33         // embedded or not and dictate texture size based on that.
34 
35         /// If true, the model should "fold" the textures, letting them repeat automatically — used for block sides.
36         int performFold;
37 
38         /// If true, the model will stay aligned to the camera on the Y camera-space axis.
39         int flatten;
40 
41         /// If true, backface culling will be disabled. Used for rendering skeleton models.
42         bool showBackfaces;
43 
44         /// Vertices making up the model.
45         Vector3[] vertices;
46 
47         /// Atlas texture fragments mapped to each vertex.
48         Rectangle[] variants;
49 
50         /// Texture coordinates within given variants.
51         Vector2[] texcoords;
52 
53         /// Bone each vertex belongs to. Each should be a fraction (bone index/bone count).
54         float[] bones;
55 
56         /// Triangles in the model, each is an index in the vertex array.
57         ushort[3][] triangles;
58 
59     }
60 
61     private {
62 
63         /// ID of the uploaded array.
64         uint vertexArrayID;
65 
66         /// ID of the bone buffer, if any.
67         uint bonesBufferID;
68 
69     }
70 
71     private static {
72 
73         /// Shader used for the model.
74         ///
75         /// Note: Currently TLS'd but it might be thread-safe — I'm not sure. Regardless though GPU data should only be
76         /// accessed from a single thread.
77         uint _shader;
78 
79         // Locations of shader uniforms
80         uint textureLoc;
81         uint transformLoc;
82         uint modelviewLoc;
83         uint projectionLoc;
84         uint colDiffuseLoc;
85         uint performFoldLoc;
86         uint flattenLoc;
87         uint matrixTextureLoc;
88 
89     }
90 
91     static immutable {
92 
93         /// Default fragment shader used to render the model.
94         ///
95         /// The data is null-terminated for C compatibility.
96         immutable vertexShader = format!"
97 
98             #version 330
99             #define PI %s
100 
101         "(raylib.PI) ~ q{
102 
103             in vec3 vertexPosition;
104             in vec2 vertexTexCoord;
105             in vec4 vertexVariantUV;
106             in float vertexBone;
107 
108             uniform mat4 transform;
109             uniform mat4 modelview;
110             uniform mat4 projection;
111             uniform int flatten;
112             uniform sampler2D matrixTexture;
113 
114             out vec2 fragTexCoord;
115             out vec4 fragVariantUV;
116             out vec2 fragAnchor;
117             flat out int angle;
118 
119             /// Rotate a vector by a quaternion.
120             /// See: https://stackoverflow.com/questions/9037174/glsl-rotation-with-a-rotation-vector/9037454#9037454
121             vec4 rotate(vec4 vector, vec4 quat) {
122 
123                 vec3 temp = cross(quat.xyz, vector.xyz) + quat.w * vector.xyz;
124                 return vector + vec4(2.0 * cross(quat.xyz, temp), 0.0);
125 
126             }
127 
128             void main() {
129 
130                 // Send vertex attributes to fragment shader
131                 fragTexCoord = vertexTexCoord;
132                 fragVariantUV = vertexVariantUV;
133 
134                 // Flatview: Camera transform (modelview) excluding vertex height
135                 mat4 flatview = modelview;
136                 flatview[0].y = 0;
137                 flatview[1].y = 1;
138                 flatview[2].y = 0;
139                 // Keep [3] to let translations (such as sharpview) through
140 
141                 // Also correct Z result, required for proper angle checking
142                 flatview[1].z = 0;
143                 flatview[2].z /= modelview[1].y;
144                 // Note: I achieved this correction by experimenting. It might not be mathematically accurate, but
145                 // it looks good enough.
146 
147                 // Set transform matrix, angle quaternion and angle index for this bone
148                 mat4 boneMatrix = mat4(1.0);
149                 vec4 boneQuat = vec4(0, 0, 0, 1);
150                 angle = 0;
151 
152                 // If we do have bone data
153                 if (!isnan(vertexBone)) {
154 
155                     // Load the matrix
156                     boneMatrix = mat4(
157                         texture2D(matrixTexture, vec2(0.0/4, vertexBone)),
158                         texture2D(matrixTexture, vec2(1.0/4, vertexBone)),
159                         texture2D(matrixTexture, vec2(2.0/4, vertexBone)),
160                         texture2D(matrixTexture, vec2(3.0/4, vertexBone))
161                     );
162 
163                     // Get the normal in camera view, only as seen from top in 2D
164                     vec2 normal = normalize(
165                         (flatview * boneMatrix * vec4(0, 0, 1, 0)).xz
166                     );
167 
168                     // Get the bone's orientation in the 2D plane (around Z axis)
169                     float orientation = atan(normal.y, normal.x) + PI / 2;
170 
171                     // Snap the angle to texture angles
172                     // TODO Use the bone's angle property
173                     float snapped = round(orientation * 2 / PI);
174 
175                     // Revert the rotation for textures
176                     angle = (4-int(snapped)) % 4;
177 
178                     // Convert angle index to radians
179                     orientation = snapped * PI / 2 / 2;
180 
181                     // Create a quaternion to counter the orientation
182                     boneQuat = vec4(0, sin(orientation), 0, cos(orientation));
183 
184                 }
185 
186                 // Transform the vertex according to bone properties
187                 vec4 position = boneMatrix * rotate(vec4(vertexPosition, 1), boneQuat);
188 
189                 // Set the anchor
190                 fragAnchor = position.xz;
191 
192                 // Regular shape
193                 if (flatten == 0) {
194 
195                     // Calculate final vertex position
196                     gl_Position = projection * modelview * transform * position;
197 
198                 }
199 
200                 // Flattened shape
201                 else {
202 
203                     // Sharp: Camera transform only affecting height
204                     mat4 sharpview = mat4(
205                         1, modelview[0].y, 0, 0,
206                         0, modelview[1].y, 0, 0,
207                         0, modelview[2].y, 1, 0,
208                         0, modelview[3].y, 0, 1
209                     );
210 
211                     // Calculate the final position in flat mode
212                     gl_Position = projection * flatview * (
213 
214                         // Get the regular position
215                         position
216 
217                         // Apply transform
218                         + sharpview * (transform * vec4(1, 1, 1, 1) - vec4(1, 1, 1, 1))
219 
220                     );
221                     // TODO Fix transform to work properly with rotations.
222 
223                 }
224 
225             }
226 
227 
228         } ~ '\0';
229 
230         /// Default fragment shader used to render the model.
231         ///
232         /// The data is null-terminated for C compatibility.
233         immutable fragmentShader = `
234 
235             #version 330
236 
237         ` ~ q{
238 
239             in vec2 fragTexCoord;
240             in vec4 fragVariantUV;
241             in vec2 fragAnchor;
242             flat in int angle;
243 
244             uniform sampler2D texture0;
245             uniform vec4 colDiffuse;
246             uniform mat4 transform;
247             uniform mat4 modelview;
248             uniform mat4 projection;
249             uniform int performFold;
250 
251             out vec4 finalColor;
252 
253             void setColor() {
254 
255                 // Get texture coordinates in the atlas
256                 vec2 coords = vec2(fragVariantUV.x, fragVariantUV.y);
257 
258                 // Get size of the texture
259                 vec2 size = vec2(fragVariantUV.z, fragVariantUV.w);
260 
261                 vec2 offset;
262 
263                 // No folding, render like normal
264                 if (performFold == 0) {
265 
266                     // Set the offset in the region with no special overrides
267                     offset = fragTexCoord * size;
268 
269                 }
270 
271                 // Perform fold
272                 else {
273 
274                     // Get texture ratio
275                     vec2 ratio = vec2(1, size.y / size.x);
276 
277                     // Get segment where the texture starts to repeat
278                     vec2 fold = vec2(0, size.y - size.x);
279 
280                     // Get offset 1. until the fold
281                     offset = min(fold, fragTexCoord * size / ratio)
282 
283                         // 2. repeat after the fold
284                         + fract(max(vec2(0), fragTexCoord - ratio + 1))*size.x;
285 
286                 }
287 
288                 // Apply angle offset
289                 offset.x += angle * size.x;
290 
291                 // Fetch the data from the texture
292                 vec4 texelColor = texture(texture0, coords + offset);
293 
294                 if (texelColor.w == 0) discard;
295 
296                 // Set the color
297                 finalColor = texelColor * colDiffuse;
298 
299             }
300 
301             void setDepth() {
302 
303                 // Change Y axis in the modelview matrix to (0, 1, 0, 0) so camera height doesn't affect depth
304                 // calculations
305                 mat4 flatview = modelview;
306                 flatview[0].y = 0;
307                 flatview[1].y = 1;
308                 flatview[2].y = 0;
309                 flatview[3].y = 0;
310                 mat4 flatform = transform;
311                 flatform[0].y = 0;
312                 flatform[1].y = 1;
313                 flatform[2].y = 0;
314                 flatform[3].y = 0;
315 
316                 // Transform the anchor in the world
317                 vec4 anchor = flatview * flatform * vec4(fragAnchor.x, 0, fragAnchor.y, 1);
318 
319                 // Get the clip space coordinates
320                 vec4 clip = projection * anchor * vec4(1, 0, 1, 1);
321 
322                 // Convert to OpenGL's value range
323                 float depth = (clip.z / clip.w + 1) / 2.0;
324                 gl_FragDepth = gl_DepthRange.diff * depth + gl_DepthRange.near;
325 
326             }
327 
328             void main() {
329 
330                 setDepth();
331                 setColor();
332 
333             }
334 
335         } ~ '\0';
336 
337     }
338 
339     void opAssign(IsodiModel model) {
340 
341         this.tupleof = model.tupleof;
342 
343     }
344 
345     /// Destroy the shader
346     static ~this() @trusted @nogc {
347 
348         // Ignore if the window isn't open anymore
349         if (!IsWindowReady) return;
350 
351         // Unload the shader (created lazily in makeShader)
352         rlUnloadShaderProgram(shader);
353 
354     }
355 
356     /// Get the shader used by the model.
357     static uint shader() @nogc => _shader;
358 
359     /// Prepare the model shader.
360     ///
361     /// Automatically performed when making the model.
362     void makeShader() @trusted @nogc {
363 
364         assert(IsWindowReady, "Cannot create shader for IsodiModel, there's no window open");
365 
366         // Ignore if already constructed
367         if (shader != 0) return;
368 
369         // Load the shader
370         _shader = rlLoadShaderCode(vertexShader.ptr, fragmentShader.ptr);
371 
372         // Find locations
373         textureLoc = rlGetLocationUniform(shader, "texture0");
374         transformLoc = rlGetLocationUniform(shader, "transform");
375         modelviewLoc = rlGetLocationUniform(shader, "modelview");
376         projectionLoc = rlGetLocationUniform(shader, "projection");
377         colDiffuseLoc = rlGetLocationUniform(shader, "colDiffuse");
378         performFoldLoc = rlGetLocationUniform(shader, "performFold");
379         flattenLoc = rlGetLocationUniform(shader, "flatten");
380         matrixTextureLoc = rlGetLocationUniform(shader, "matrixTexture");
381 
382     }
383 
384     /// Upload the model to the GPU.
385     void upload() @trusted
386     in {
387 
388         // Check buffers
389         assert(vertexArrayID == 0, "The model has already been uploaded");  // TODO: allow updates
390         assert(vertices.length <= ushort.max, "Model cannot be drawn, too many vertices exist");
391         assert(variants.length == vertices.length,
392             format!"Variant count (%s) doesn't match vertex count (%s)"(variants.length, vertices.length));
393         assert(texcoords.length == vertices.length,
394             format!"Texcoord count (%s) doesn't match vertex count (%s)"(texcoords.length, vertices.length));
395 
396         // Check matrixTexture
397         if (matrixTexture.id != 0) {
398 
399             assert(matrixTexture.width == 4,
400                 format!"Matrix texture width (%s) must be 4."(matrixTexture.width));
401             assert(matrixTexture.height != 0, format!"Matrix texture height must not be 0.");
402             assert(bones.length == vertices.length,
403                 format!"Bone count (%s) doesn't match vertex count (%s)"(bones.length, vertices.length));
404 
405         }
406 
407         else assert(bones.length == 0, "Vertex bone definitions are present, but no matrixTexture is attached");
408 
409     }
410     do {
411 
412         uint registerBuffer(T)(T[] arr, const char* attribute, int type) {
413 
414             // Determine size of the type
415             static if (__traits(compiles, T.tupleof)) enum length = T.tupleof.length;
416             else enum length = 1;
417 
418             // Find location in the shader
419             const location = rlGetLocationAttrib(shader, attribute);
420 
421             // If the array is empty
422             if (arr.length == 0) {
423 
424                 // For some reason type passed to rlSetVertexAttributeDefault doesn't match the one passed to
425                 // rlSetVertexAttribute. If you look into the Raylib code then you'll notice that the type argument is
426                 // actually completely unnecessary, but must match the length.
427                 // For this reason, this code path is only implemented for this case.
428                 bool ass = length == 1;
429                 assert(ass);
430                 // BTW DMD incorrectly emits a warning here, this is the reason silenceWarnings is set.
431 
432                 const value = T.init;
433 
434                 // Set a default value for the attribute
435                 rlSetVertexAttributeDefault(location, &value, rlShaderAttributeDataType.RL_SHADER_ATTRIB_FLOAT, length);
436                 rlDisableVertexAttribute(location);
437 
438                 return 0;
439 
440             }
441 
442             // Load the buffer
443             auto bufferID = rlLoadVertexBuffer(arr.ptr, cast(int) (arr.length * T.sizeof), false);
444 
445             // Stop if the attribute isn't set
446             if (location == -1) return bufferID;
447 
448             // Assign to a shader attribute
449             rlSetVertexAttribute(location, length, type, 0, 0, null);
450 
451             // Turn the attribute on
452             rlEnableVertexAttribute(location);
453 
454             return bufferID;
455 
456         }
457 
458         // Prepare the shader
459         makeShader();
460 
461         // Create the vertex array
462         vertexArrayID = rlLoadVertexArray();
463         rlEnableVertexArray(vertexArrayID);
464         scope (exit) rlDisableVertexArray;
465 
466         // Send vertex positions
467         registerBuffer(vertices, "vertexPosition", RL_FLOAT);
468         registerBuffer(variants, "vertexVariantUV", RL_FLOAT);
469         registerBuffer(texcoords, "vertexTexCoord", RL_FLOAT);
470         bonesBufferID = registerBuffer(bones, "vertexBone", RL_FLOAT);
471         rlLoadVertexBufferElement(triangles.ptr, cast(int) (triangles.length * 3 * ushort.sizeof), false);
472 
473 
474     }
475 
476     /// Draw the model.
477     ///
478     /// Note: Each vertex in the model will be drawn as if it was on Y=0. It is recommended you draw other Isodi objects
479     /// in order of height.
480     void draw() const @trusted @nogc
481     in (vertices.length != 0, "This model cannot be drawn, there's no vertices")
482     in (vertices.length <= ushort.max, "This model cannot be drawn, too many vertices exist")
483     do {
484 
485         alias Type = rlShaderUniformDataType;
486 
487         // Update the shader
488         rlEnableShader(shader);
489         scope (exit) rlDisableShader();
490 
491         // Set data
492         rlSetUniform(performFoldLoc, &performFold, Type.RL_SHADER_UNIFORM_INT, 1);
493         rlSetUniform(flattenLoc, &flatten, Type.RL_SHADER_UNIFORM_INT, 1);
494 
495         // Set colDiffuse
496         rlSetUniform(colDiffuseLoc, &properties.tint, Type.RL_SHADER_UNIFORM_VEC4, 1);
497 
498         /// Set active texture.
499         void setTexture(int slot, int loc, Texture2D texture) {
500 
501             // Ignore if there isn't any
502             if (texture.id == 0) return;
503 
504             rlActiveTextureSlot(slot);
505             rlEnableTexture(texture.id);
506             rlSetUniform(loc, &slot, Type.RL_SHADER_UNIFORM_INT, 1);
507 
508         }
509 
510         /// Disable the texture.
511         void unsetTexture(int slot) {
512             rlActiveTextureSlot(slot);
513             rlDisableTexture;
514         }
515 
516         // Set texture to use
517         setTexture(0, textureLoc, texture);
518         scope (exit) unsetTexture(0);
519 
520         // Set matrix texture
521         setTexture(1, matrixTextureLoc, matrixTexture);
522         scope (exit) unsetTexture(1);
523 
524         // Set transform matrix
525         const transformMatrix = properties.transform
526             .MatrixMultiply(rlGetMatrixTransform);
527         rlSetUniformMatrix(transformLoc, transformMatrix);
528 
529         // Set model view & projection matrices
530         rlSetUniformMatrix(modelviewLoc, rlGetMatrixModelview);
531         rlSetUniformMatrix(projectionLoc, rlGetMatrixProjection);
532 
533         // Enable the vertex array
534         const enabled = rlEnableVertexArray(vertexArrayID);
535         assert(enabled, "Failed to enable a vertex array");
536         scope (exit) rlDisableVertexArray;
537 
538         // Toggle backface culling
539         if (showBackfaces) rlDisableBackfaceCulling();
540         scope (exit) rlEnableBackfaceCulling();
541         // Note: Reordering the vertices in the vertex shader in skeleton models doesn't appear achievable without
542         // sending otherwise unnecessary data. So we disable it entirely instead.
543 
544         rlDrawVertexArrayElements(0, cast(int) triangles.length * 3, null);
545 
546     }
547 
548 }