1 /* 2 Inochi2D Composite Node 3 4 Copyright © 2020, Inochi2D Project 5 Distributed under the 2-Clause BSD License, see LICENSE file. 6 7 Authors: Luna Nielsen 8 */ 9 module inochi2d.core.nodes.composite; 10 import inochi2d.core.nodes.common; 11 import inochi2d.core.nodes; 12 import inochi2d.fmt; 13 import inochi2d.core; 14 import inochi2d.math; 15 import bindbc.opengl; 16 import std.exception; 17 import std.algorithm.sorting; 18 19 private { 20 GLuint cVAO; 21 GLuint cBuffer; 22 Shader cShader; 23 Shader cShaderMask; 24 25 GLint gopacity; 26 GLint gMultColor; 27 GLint gScreenColor; 28 29 GLint mthreshold; 30 GLint mopacity; 31 } 32 33 package(inochi2d) { 34 void inInitComposite() { 35 inRegisterNodeType!Composite; 36 37 version(InDoesRender) { 38 cShader = new Shader( 39 import("basic/composite.vert"), 40 import("basic/composite.frag") 41 ); 42 43 cShader.use(); 44 gopacity = cShader.getUniformLocation("opacity"); 45 gMultColor = cShader.getUniformLocation("multColor"); 46 gScreenColor = cShader.getUniformLocation("screenColor"); 47 cShader.setUniform(cShader.getUniformLocation("albedo"), 0); 48 cShader.setUniform(cShader.getUniformLocation("emissive"), 1); 49 cShader.setUniform(cShader.getUniformLocation("bumpmap"), 2); 50 51 cShaderMask = new Shader( 52 import("basic/composite.vert"), 53 import("basic/composite-mask.frag") 54 ); 55 cShaderMask.use(); 56 mthreshold = cShader.getUniformLocation("threshold"); 57 mopacity = cShader.getUniformLocation("opacity"); 58 59 glGenVertexArrays(1, &cVAO); 60 glGenBuffers(1, &cBuffer); 61 62 // Clip space vertex data since we'll just be superimposing 63 // Our composite framebuffer over the main framebuffer 64 float[] vertexData = [ 65 // verts 66 -1f, -1f, 67 -1f, 1f, 68 1f, -1f, 69 1f, -1f, 70 -1f, 1f, 71 1f, 1f, 72 73 // uvs 74 0f, 0f, 75 0f, 1f, 76 1f, 0f, 77 1f, 0f, 78 0f, 1f, 79 1f, 1f, 80 ]; 81 82 glBindVertexArray(cVAO); 83 glBindBuffer(GL_ARRAY_BUFFER, cBuffer); 84 glBufferData(GL_ARRAY_BUFFER, float.sizeof*vertexData.length, vertexData.ptr, GL_STATIC_DRAW); 85 } 86 } 87 } 88 89 /** 90 Composite Node 91 */ 92 @TypeId("Composite") 93 class Composite : Node { 94 private: 95 96 this() { } 97 98 void drawContents() { 99 100 // Optimization: Nothing to be drawn, skip context switching 101 if (subParts.length == 0) return; 102 103 inBeginComposite(); 104 105 foreach(Part child; subParts) { 106 child.drawOne(); 107 } 108 109 inEndComposite(); 110 } 111 112 /* 113 RENDERING 114 */ 115 void drawSelf() { 116 if (subParts.length == 0) return; 117 118 glBindVertexArray(cVAO); 119 120 cShader.use(); 121 cShader.setUniform(gopacity, clamp(offsetOpacity * opacity, 0, 1)); 122 incCompositePrepareRender(); 123 124 vec3 clampedColor = tint; 125 if (!offsetTint.x.isNaN) clampedColor.x = clamp(tint.x*offsetTint.x, 0, 1); 126 if (!offsetTint.y.isNaN) clampedColor.y = clamp(tint.y*offsetTint.y, 0, 1); 127 if (!offsetTint.z.isNaN) clampedColor.z = clamp(tint.z*offsetTint.z, 0, 1); 128 cShader.setUniform(gMultColor, clampedColor); 129 130 clampedColor = screenTint; 131 if (!offsetScreenTint.x.isNaN) clampedColor.x = clamp(screenTint.x+offsetScreenTint.x, 0, 1); 132 if (!offsetScreenTint.y.isNaN) clampedColor.y = clamp(screenTint.y+offsetScreenTint.y, 0, 1); 133 if (!offsetScreenTint.z.isNaN) clampedColor.z = clamp(screenTint.z+offsetScreenTint.z, 0, 1); 134 cShader.setUniform(gScreenColor, clampedColor); 135 inSetBlendMode(blendingMode); 136 137 // Enable points array 138 glEnableVertexAttribArray(0); 139 glEnableVertexAttribArray(1); 140 glBindBuffer(GL_ARRAY_BUFFER, cBuffer); 141 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, null); 142 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, cast(void*)(12*float.sizeof)); 143 144 // Bind the texture 145 glDrawArrays(GL_TRIANGLES, 0, 6); 146 } 147 148 void selfSort() { 149 import std.math : cmp; 150 sort!((a, b) => cmp( 151 a.zSort, 152 b.zSort) > 0)(subParts); 153 } 154 155 void scanPartsRecurse(ref Node node) { 156 157 // Don't need to scan null nodes 158 if (node is null) return; 159 160 // Do the main check 161 if (Part part = cast(Part)node) { 162 subParts ~= part; 163 foreach(child; part.children) { 164 scanPartsRecurse(child); 165 } 166 167 } else { 168 169 // Non-part nodes just need to be recursed through, 170 // they don't draw anything. 171 foreach(child; node.children) { 172 scanPartsRecurse(child); 173 } 174 } 175 } 176 177 protected: 178 Part[] subParts; 179 180 void renderMask() { 181 inBeginComposite(); 182 183 // Enable writing to stencil buffer and disable writing to color buffer 184 glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); 185 glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); 186 glStencilFunc(GL_ALWAYS, 1, 0xFF); 187 glStencilMask(0xFF); 188 189 foreach(Part child; subParts) { 190 child.drawOneDirect(true); 191 } 192 193 // Disable writing to stencil buffer and enable writing to color buffer 194 glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); 195 inEndComposite(); 196 197 198 glBindVertexArray(cVAO); 199 cShaderMask.use(); 200 cShaderMask.setUniform(mopacity, opacity); 201 cShaderMask.setUniform(mthreshold, threshold); 202 glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); 203 204 // Enable points array 205 glEnableVertexAttribArray(0); 206 glEnableVertexAttribArray(1); 207 glBindBuffer(GL_ARRAY_BUFFER, cBuffer); 208 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, null); 209 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, cast(void*)(12*float.sizeof)); 210 211 glActiveTexture(GL_TEXTURE0); 212 glBindTexture(GL_TEXTURE_2D, inGetCompositeImage()); 213 glDrawArrays(GL_TRIANGLES, 0, 6); 214 } 215 216 override 217 void serializeSelfImpl(ref InochiSerializer serializer, bool recursive=true) { 218 super.serializeSelfImpl(serializer, recursive); 219 220 serializer.putKey("blend_mode"); 221 serializer.serializeValue(blendingMode); 222 223 serializer.putKey("tint"); 224 tint.serialize(serializer); 225 226 serializer.putKey("screenTint"); 227 screenTint.serialize(serializer); 228 229 serializer.putKey("mask_threshold"); 230 serializer.putValue(threshold); 231 232 serializer.putKey("opacity"); 233 serializer.putValue(opacity); 234 235 if (masks.length > 0) { 236 serializer.putKey("masks"); 237 auto state = serializer.arrayBegin(); 238 foreach(m; masks) { 239 serializer.elemBegin; 240 serializer.serializeValue(m); 241 } 242 serializer.arrayEnd(state); 243 } 244 } 245 246 override 247 SerdeException deserializeFromFghj(Fghj data) { 248 249 // Older models may not have these tags 250 if (!data["opacity"].isEmpty) data["opacity"].deserializeValue(this.opacity); 251 if (!data["mask_threshold"].isEmpty) data["mask_threshold"].deserializeValue(this.threshold); 252 if (!data["tint"].isEmpty) deserialize(this.tint, data["tint"]); 253 if (!data["screenTint"].isEmpty) deserialize(this.screenTint, data["screenTint"]); 254 if (!data["blend_mode"].isEmpty) data["blend_mode"].deserializeValue(this.blendingMode); 255 if (!data["masks"].isEmpty) data["masks"].deserializeValue(this.masks); 256 257 return super.deserializeFromFghj(data); 258 } 259 260 // 261 // PARAMETER OFFSETS 262 // 263 float offsetOpacity = 1; 264 vec3 offsetTint = vec3(0); 265 vec3 offsetScreenTint = vec3(0); 266 267 override 268 string typeId() { return "Composite"; } 269 270 // TODO: Cache this 271 size_t maskCount() { 272 size_t c; 273 foreach(m; masks) if (m.mode == MaskingMode.Mask) c++; 274 return c; 275 } 276 277 size_t dodgeCount() { 278 size_t c; 279 foreach(m; masks) if (m.mode == MaskingMode.DodgeMask) c++; 280 return c; 281 } 282 283 public: 284 285 /** 286 The blending mode 287 */ 288 BlendMode blendingMode; 289 290 /** 291 The opacity of the composite 292 */ 293 float opacity = 1; 294 295 /** 296 The threshold for rendering masks 297 */ 298 float threshold = 0.5; 299 300 /** 301 Multiplicative tint color 302 */ 303 vec3 tint = vec3(1, 1, 1); 304 305 /** 306 Screen tint color 307 */ 308 vec3 screenTint = vec3(0, 0, 0); 309 310 /** 311 List of masks to apply 312 */ 313 MaskBinding[] masks; 314 315 316 /** 317 Constructs a new mask 318 */ 319 this(Node parent = null) { 320 this(inCreateUUID(), parent); 321 } 322 323 /** 324 Constructs a new composite 325 */ 326 this(uint uuid, Node parent = null) { 327 super(uuid, parent); 328 } 329 330 override 331 bool hasParam(string key) { 332 if (super.hasParam(key)) return true; 333 334 switch(key) { 335 case "opacity": 336 case "tint.r": 337 case "tint.g": 338 case "tint.b": 339 case "screenTint.r": 340 case "screenTint.g": 341 case "screenTint.b": 342 return true; 343 default: 344 return false; 345 } 346 } 347 348 override 349 float getDefaultValue(string key) { 350 // Skip our list of our parent already handled it 351 float def = super.getDefaultValue(key); 352 if (!isNaN(def)) return def; 353 354 switch(key) { 355 case "opacity": 356 case "tint.r": 357 case "tint.g": 358 case "tint.b": 359 return 1; 360 case "screenTint.r": 361 case "screenTint.g": 362 case "screenTint.b": 363 return 0; 364 default: return float(); 365 } 366 } 367 368 override 369 bool setValue(string key, float value) { 370 371 // Skip our list of our parent already handled it 372 if (super.setValue(key, value)) return true; 373 374 switch(key) { 375 case "opacity": 376 offsetOpacity *= value; 377 return true; 378 case "tint.r": 379 offsetTint.x += value; 380 return true; 381 case "tint.g": 382 offsetTint.y += value; 383 return true; 384 case "tint.b": 385 offsetTint.z += value; 386 return true; 387 case "screenTint.r": 388 offsetScreenTint.x += value; 389 return true; 390 case "screenTint.g": 391 offsetScreenTint.y += value; 392 return true; 393 case "screenTint.b": 394 offsetScreenTint.z += value; 395 return true; 396 default: return false; 397 } 398 } 399 400 override 401 float getValue(string key) { 402 switch(key) { 403 case "opacity": return offsetOpacity; 404 case "tint.r": return offsetTint.x; 405 case "tint.g": return offsetTint.y; 406 case "tint.b": return offsetTint.z; 407 case "screenTint.r": return offsetScreenTint.x; 408 case "screenTint.g": return offsetScreenTint.y; 409 case "screenTint.b": return offsetScreenTint.z; 410 default: return super.getValue(key); 411 } 412 } 413 414 bool isMaskedBy(Drawable drawable) { 415 foreach(mask; masks) { 416 if (mask.maskSrc.uuid == drawable.uuid) return true; 417 } 418 return false; 419 } 420 421 ptrdiff_t getMaskIdx(Drawable drawable) { 422 if (drawable is null) return -1; 423 foreach(i, ref mask; masks) { 424 if (mask.maskSrc.uuid == drawable.uuid) return i; 425 } 426 return -1; 427 } 428 429 ptrdiff_t getMaskIdx(uint uuid) { 430 foreach(i, ref mask; masks) { 431 if (mask.maskSrc.uuid == uuid) return i; 432 } 433 return -1; 434 } 435 436 override 437 void beginUpdate() { 438 offsetOpacity = 1; 439 offsetTint = vec3(1, 1, 1); 440 offsetScreenTint = vec3(0, 0, 0); 441 super.beginUpdate(); 442 } 443 444 override 445 void drawOne() { 446 if (!enabled) return; 447 448 this.selfSort(); 449 this.drawContents(); 450 451 size_t cMasks = maskCount; 452 453 if (masks.length > 0) { 454 inBeginMask(cMasks > 0); 455 456 foreach(ref mask; masks) { 457 mask.maskSrc.renderMask(mask.mode == MaskingMode.DodgeMask); 458 } 459 460 inBeginMaskContent(); 461 462 // We are the content 463 this.drawSelf(); 464 465 inEndMask(); 466 return; 467 } 468 469 // No masks, draw normally 470 super.drawOne(); 471 this.drawSelf(); 472 } 473 474 override 475 void draw() { 476 if (!enabled) return; 477 this.drawOne(); 478 } 479 480 override 481 void finalize() { 482 super.finalize(); 483 484 MaskBinding[] validMasks; 485 foreach(i; 0..masks.length) { 486 if (Drawable nMask = puppet.find!Drawable(masks[i].maskSrcUUID)) { 487 masks[i].maskSrc = nMask; 488 validMasks ~= masks[i]; 489 } 490 } 491 492 // Remove invalid masks 493 masks = validMasks; 494 } 495 496 /** 497 Scans for parts to render 498 */ 499 void scanParts() { 500 subParts.length = 0; 501 if (children.length > 0) { 502 scanPartsRecurse(children[0].parent); 503 } 504 } 505 }