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 }