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 glDrawBuffers(3, [GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2].ptr); 119 glBindVertexArray(cVAO); 120 121 cShader.use(); 122 cShader.setUniform(gopacity, clamp(offsetOpacity * opacity, 0, 1)); 123 incCompositePrepareRender(); 124 125 vec3 clampedColor = tint; 126 if (!offsetTint.x.isNaN) clampedColor.x = clamp(tint.x*offsetTint.x, 0, 1); 127 if (!offsetTint.y.isNaN) clampedColor.y = clamp(tint.y*offsetTint.y, 0, 1); 128 if (!offsetTint.z.isNaN) clampedColor.z = clamp(tint.z*offsetTint.z, 0, 1); 129 cShader.setUniform(gMultColor, clampedColor); 130 131 clampedColor = screenTint; 132 if (!offsetScreenTint.x.isNaN) clampedColor.x = clamp(screenTint.x+offsetScreenTint.x, 0, 1); 133 if (!offsetScreenTint.y.isNaN) clampedColor.y = clamp(screenTint.y+offsetScreenTint.y, 0, 1); 134 if (!offsetScreenTint.z.isNaN) clampedColor.z = clamp(screenTint.z+offsetScreenTint.z, 0, 1); 135 cShader.setUniform(gScreenColor, clampedColor); 136 inSetBlendMode(blendingMode, true); 137 138 // Enable points array 139 glEnableVertexAttribArray(0); 140 glEnableVertexAttribArray(1); 141 glBindBuffer(GL_ARRAY_BUFFER, cBuffer); 142 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, null); 143 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, cast(void*)(12*float.sizeof)); 144 145 // Bind the texture 146 glDrawArrays(GL_TRIANGLES, 0, 6); 147 } 148 149 void selfSort() { 150 import std.math : cmp; 151 sort!((a, b) => cmp( 152 a.zSort, 153 b.zSort) > 0)(subParts); 154 } 155 156 void scanPartsRecurse(ref Node node) { 157 158 // Don't need to scan null nodes 159 if (node is null) return; 160 161 // Do the main check 162 if (Part part = cast(Part)node) { 163 subParts ~= part; 164 foreach(child; part.children) { 165 scanPartsRecurse(child); 166 } 167 168 } else { 169 170 // Non-part nodes just need to be recursed through, 171 // they don't draw anything. 172 foreach(child; node.children) { 173 scanPartsRecurse(child); 174 } 175 } 176 } 177 178 protected: 179 Part[] subParts; 180 181 void renderMask() { 182 inBeginComposite(); 183 184 // Enable writing to stencil buffer and disable writing to color buffer 185 glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); 186 glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); 187 glStencilFunc(GL_ALWAYS, 1, 0xFF); 188 glStencilMask(0xFF); 189 190 foreach(Part child; subParts) { 191 child.drawOneDirect(true); 192 } 193 194 // Disable writing to stencil buffer and enable writing to color buffer 195 glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); 196 inEndComposite(); 197 198 199 glBindVertexArray(cVAO); 200 cShaderMask.use(); 201 cShaderMask.setUniform(mopacity, opacity); 202 cShaderMask.setUniform(mthreshold, threshold); 203 glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); 204 205 // Enable points array 206 glEnableVertexAttribArray(0); 207 glEnableVertexAttribArray(1); 208 glBindBuffer(GL_ARRAY_BUFFER, cBuffer); 209 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, null); 210 glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, cast(void*)(12*float.sizeof)); 211 212 glActiveTexture(GL_TEXTURE0); 213 glBindTexture(GL_TEXTURE_2D, inGetCompositeImage()); 214 glDrawArrays(GL_TRIANGLES, 0, 6); 215 } 216 217 override 218 void serializeSelfImpl(ref InochiSerializer serializer, bool recursive=true) { 219 super.serializeSelfImpl(serializer, recursive); 220 221 serializer.putKey("blend_mode"); 222 serializer.serializeValue(blendingMode); 223 224 serializer.putKey("tint"); 225 tint.serialize(serializer); 226 227 serializer.putKey("screenTint"); 228 screenTint.serialize(serializer); 229 230 serializer.putKey("mask_threshold"); 231 serializer.putValue(threshold); 232 233 serializer.putKey("opacity"); 234 serializer.putValue(opacity); 235 236 serializer.putKey("propagate_meshgroup"); 237 serializer.serializeValue(propagateMeshGroup); 238 239 if (masks.length > 0) { 240 serializer.putKey("masks"); 241 auto state = serializer.arrayBegin(); 242 foreach(m; masks) { 243 serializer.elemBegin; 244 serializer.serializeValue(m); 245 } 246 serializer.arrayEnd(state); 247 248 } 249 } 250 251 override 252 SerdeException deserializeFromFghj(Fghj data) { 253 254 // Older models may not have these tags 255 if (!data["opacity"].isEmpty) data["opacity"].deserializeValue(this.opacity); 256 if (!data["mask_threshold"].isEmpty) data["mask_threshold"].deserializeValue(this.threshold); 257 if (!data["tint"].isEmpty) deserialize(this.tint, data["tint"]); 258 if (!data["screenTint"].isEmpty) deserialize(this.screenTint, data["screenTint"]); 259 if (!data["blend_mode"].isEmpty) data["blend_mode"].deserializeValue(this.blendingMode); 260 if (!data["masks"].isEmpty) data["masks"].deserializeValue(this.masks); 261 if (!data["propagate_meshgroup"].isEmpty) 262 data["propagate_meshgroup"].deserializeValue(propagateMeshGroup); 263 else // falls back to legacy default 264 propagateMeshGroup = false; 265 266 return super.deserializeFromFghj(data); 267 } 268 269 // 270 // PARAMETER OFFSETS 271 // 272 float offsetOpacity = 1; 273 vec3 offsetTint = vec3(0); 274 vec3 offsetScreenTint = vec3(0); 275 276 override 277 string typeId() { return "Composite"; } 278 279 // TODO: Cache this 280 size_t maskCount() { 281 size_t c; 282 foreach(m; masks) if (m.mode == MaskingMode.Mask) c++; 283 return c; 284 } 285 286 size_t dodgeCount() { 287 size_t c; 288 foreach(m; masks) if (m.mode == MaskingMode.DodgeMask) c++; 289 return c; 290 } 291 292 override 293 void preProcess() { 294 if (!propagateMeshGroup) 295 Node.preProcess(); 296 } 297 298 override 299 void postProcess() { 300 if (!propagateMeshGroup) 301 Node.postProcess(); 302 } 303 304 public: 305 bool propagateMeshGroup = true; 306 307 /** 308 The blending mode 309 */ 310 BlendMode blendingMode; 311 312 /** 313 The opacity of the composite 314 */ 315 float opacity = 1; 316 317 /** 318 The threshold for rendering masks 319 */ 320 float threshold = 0.5; 321 322 /** 323 Multiplicative tint color 324 */ 325 vec3 tint = vec3(1, 1, 1); 326 327 /** 328 Screen tint color 329 */ 330 vec3 screenTint = vec3(0, 0, 0); 331 332 /** 333 List of masks to apply 334 */ 335 MaskBinding[] masks; 336 337 338 /** 339 Constructs a new mask 340 */ 341 this(Node parent = null) { 342 this(inCreateUUID(), parent); 343 } 344 345 /** 346 Constructs a new composite 347 */ 348 this(uint uuid, Node parent = null) { 349 super(uuid, parent); 350 } 351 352 override 353 bool hasParam(string key) { 354 if (super.hasParam(key)) return true; 355 356 switch(key) { 357 case "opacity": 358 case "tint.r": 359 case "tint.g": 360 case "tint.b": 361 case "screenTint.r": 362 case "screenTint.g": 363 case "screenTint.b": 364 return true; 365 default: 366 return false; 367 } 368 } 369 370 override 371 float getDefaultValue(string key) { 372 // Skip our list of our parent already handled it 373 float def = super.getDefaultValue(key); 374 if (!isNaN(def)) return def; 375 376 switch(key) { 377 case "opacity": 378 case "tint.r": 379 case "tint.g": 380 case "tint.b": 381 return 1; 382 case "screenTint.r": 383 case "screenTint.g": 384 case "screenTint.b": 385 return 0; 386 default: return float(); 387 } 388 } 389 390 override 391 bool setValue(string key, float value) { 392 393 // Skip our list of our parent already handled it 394 if (super.setValue(key, value)) return true; 395 396 switch(key) { 397 case "opacity": 398 offsetOpacity *= value; 399 return true; 400 case "tint.r": 401 offsetTint.x += value; 402 return true; 403 case "tint.g": 404 offsetTint.y += value; 405 return true; 406 case "tint.b": 407 offsetTint.z += value; 408 return true; 409 case "screenTint.r": 410 offsetScreenTint.x += value; 411 return true; 412 case "screenTint.g": 413 offsetScreenTint.y += value; 414 return true; 415 case "screenTint.b": 416 offsetScreenTint.z += value; 417 return true; 418 default: return false; 419 } 420 } 421 422 override 423 float getValue(string key) { 424 switch(key) { 425 case "opacity": return offsetOpacity; 426 case "tint.r": return offsetTint.x; 427 case "tint.g": return offsetTint.y; 428 case "tint.b": return offsetTint.z; 429 case "screenTint.r": return offsetScreenTint.x; 430 case "screenTint.g": return offsetScreenTint.y; 431 case "screenTint.b": return offsetScreenTint.z; 432 default: return super.getValue(key); 433 } 434 } 435 436 bool isMaskedBy(Drawable drawable) { 437 foreach(mask; masks) { 438 if (mask.maskSrc.uuid == drawable.uuid) return true; 439 } 440 return false; 441 } 442 443 ptrdiff_t getMaskIdx(Drawable drawable) { 444 if (drawable is null) return -1; 445 foreach(i, ref mask; masks) { 446 if (mask.maskSrc.uuid == drawable.uuid) return i; 447 } 448 return -1; 449 } 450 451 ptrdiff_t getMaskIdx(uint uuid) { 452 foreach(i, ref mask; masks) { 453 if (mask.maskSrc.uuid == uuid) return i; 454 } 455 return -1; 456 } 457 458 override 459 void beginUpdate() { 460 offsetOpacity = 1; 461 offsetTint = vec3(1, 1, 1); 462 offsetScreenTint = vec3(0, 0, 0); 463 super.beginUpdate(); 464 } 465 466 override 467 void drawOne() { 468 if (!enabled) return; 469 470 this.selfSort(); 471 this.drawContents(); 472 473 size_t cMasks = maskCount; 474 475 if (masks.length > 0) { 476 inBeginMask(cMasks > 0); 477 478 foreach(ref mask; masks) { 479 mask.maskSrc.renderMask(mask.mode == MaskingMode.DodgeMask); 480 } 481 482 inBeginMaskContent(); 483 484 // We are the content 485 this.drawSelf(); 486 487 inEndMask(); 488 return; 489 } 490 491 // No masks, draw normally 492 super.drawOne(); 493 this.drawSelf(); 494 } 495 496 override 497 void draw() { 498 if (!enabled) return; 499 this.drawOne(); 500 } 501 502 override 503 void finalize() { 504 super.finalize(); 505 506 MaskBinding[] validMasks; 507 foreach(i; 0..masks.length) { 508 if (Drawable nMask = puppet.find!Drawable(masks[i].maskSrcUUID)) { 509 masks[i].maskSrc = nMask; 510 validMasks ~= masks[i]; 511 } 512 } 513 514 // Remove invalid masks 515 masks = validMasks; 516 } 517 518 /** 519 Scans for parts to render 520 */ 521 void scanParts() { 522 subParts.length = 0; 523 if (children.length > 0) { 524 scanPartsRecurse(children[0].parent); 525 } 526 } 527 }