diff --git a/extensions/community/ToonShader.json b/extensions/community/ToonShader.json new file mode 100644 index 000000000..f78539225 --- /dev/null +++ b/extensions/community/ToonShader.json @@ -0,0 +1,2149 @@ +{ + "author": "", + "category": "General", + "extensionNamespace": "", + "fullName": "cartoon shader", + "gdevelopVersion": "", + "helpPath": "https://threejs.org/docs/#api/en/materials/MeshPhysicalMaterial", + "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAyMy4wLjMsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iSWNvbnMiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgMzIgMzIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDMyIDMyOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0i dGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDt9DQoJLnN0MXtmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO30NCgkuc3Qye2ZpbGw6bm9uZTtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLWRhc2hhcnJheTo2LDY7fQ0KCS5zdDN7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtZGFzaGFycmF5OjQsNDt9DQoJLnN0NHtmaWxsOm5vbmU7c3Ryb2tlOiMwMDAwMDA7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7fQ0KCS5zdDV7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1kYXNoYXJyYXk6My4xMDgxL DMuMTA4MTt9DQoJDQoJCS5zdDZ7ZmlsbDpub25lO3N0cm9rZTojMDAwMDAwO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDoxMDtzdHJva2UtZGFzaGFycmF5OjQsMzt9DQo8L3N0eWxlPg0KPGNpcmNsZSBjbGFzcz0ic3QwIiBjeD0iMTMiIGN5PSIxMyIgcj0iMTAiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyMCIgeTE9IjIwIiB4Mj0iMjYuMSIgeTI9IjI2LjEiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIxNiIgeTE9IjIzIiB4Mj0iMjEuOCIgeTI9IjI4LjkiLz4NCjxsaW5lIGNsYXNzPSJzdDAiIHgxPSIyMyIgeTE9IjE2IiB4Mj0iMjguOSIgeTI9IjIxLjgiLz4NCjxwYXRoIGNsYXNzPSJzdDAiIGQ9Ik0yMi40LDkuNmMxLjMsMC41LDIuNiwxLjMsMy43LDIuM2MzLjksMy45LDMuOSwxMC4yLDAsMTQuMXMtMTAuMiwzLjktMTQuMSwwYy0xLjEtMS4xLTEuOC0yLjMtMi4zLTMuNyIvPg0KPC9zdmc+DQo=", + "name": "ToonShader", + "previewIconUrl": "https://asset-resources.gdevelop.io/public-resources/Icons/Line Hero Pack/Master/SVG/Graphic Design/72d62d2888b4617f55edbefa73aa471fe8c846bb8552ce6a368fc05327dc8fa6_Graphic Design_drop_shadow.svg", + "shortDescription": "Toon Shading: Cel-shading with custom gradients.\nCartoon Outline: Variable outlines with skinning support.\nRim Lighting: Fresnel edge glow.\nSpecular Highlights: Sharp cartoon shiny spots.\nEmission: Glowing effects for eyes and magic.", + "version": "0.1.0", + "description": [ + "This extension includes several actions required to achieve a complete, integrated cartoon (Cel-Shaded) look on your 3D objects.", + "", + "1. Toon Shading (Material)", + "This action applies the core cartoon shading material to the object, eliminating soft shadow transitions and replacing them with sharp, distinct color steps.", + "", + "Action: Make the object toon shaded (Make PARAM1's material Toon , Color: PARAM2 Gradient: PARAM3).", + "", + "Three.js Material Used: THREE.MeshToonMaterial is applied.", + "", + "Parameters:", + "", + "Object: The target 3D object (e.g., Scene3D::Cube3DObject).", + "", + "Color: The primary base color of the object.", + "", + "Gradient: An image resource that contains the shading steps. The recommendation is to use an image with only 3 or 5 pixels of different brightness levels (a small horizontal strip of light and dark colors) to define how lighting transitions into shadows.", + "", + "2. Cartoon Outline (Effects)", + "This action adds clear outline edges around the 3D object, which is essential for completing the cartoon or anime aesthetic.", + "", + "Actions: Add/Change Outline and Remove outline.", + "", + "Mechanism: The action works by creating supplementary elements on top of the original object: a fill overlay and outline meshes using a backface rendering technique that properly supports variable thickness and depth testing.", + "", + "Parameters and Control:", + "", + "Fill Color: The color of the front overlay.", + "", + "Outline Color: The color of the outline.", + "", + "Fill/Outline Opacity: The transparency level for both the fill and the outline.", + "", + "Outline Thickness: Specifies the outline width using a scaling factor (higher values = thicker outline).", + "", + "Render Order: Allows you to set the Fill and Outline Render Order to ensure the effect layers correctly over the original object.", + "", + "3. Rim Lighting / Fresnel Effect", + "This action adds a glow around the silhouette of the object, which highlights its shape and adds depth, common in high-quality anime and stylized games.", + "", + "Action: Apply rim lighting (Color: PARAM2, Intensity: PARAM3, Spread: PARAM4, Sharpness: PARAM5).", + "", + "Parameters:", + "Intensity: How bright the rim is (e.g. 1.0 to 10.0).", + "Spread: Controls the falloff (higher values make the rim thinner).", + "Sharpness: Controls how crisp the edge is (0.0 for smooth, 1.0 for sharp toon style).", + "", + "4. Emission / Glow", + "Make objects glow with a specific color and intensity. You can also use a texture map to make only specific parts (like eyes) glow.", + "", + "Action: Apply emission to Object (Color: PARAM2, Intensity: PARAM3, Texture: PARAM4)." + ], + "origin": { + "identifier": "Toon Shader", + "name": "gdevelop-extension-store" + }, + "tags": [ + "material", + "texture", + "3D", + "wireframe", + "Shader" + ], + "authorIds": [ + "sXdoMxxHF7hAXkEPRO8oZcfnBgC2" + ], + "dependencies": [], + "globalVariables": [], + "sceneVariables": [], + "eventsFunctions": [ + { + "description": "Add/change a normal map for an object face", + "fullName": "Normal texture", + "functionType": "Action", + "group": "Effects", + "name": "Normals", + "private": true, + "sentence": "", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "objects.forEach(object => {", + " const threeObject = object.get3DRendererObject();", + " ", + " if (threeObject) {", + " const normalMapTexture = eventsFunctionContext.getArgument(\"Texture\");", + " const materialIndex = eventsFunctionContext.getArgument(\"Face\");", + " ", + " // Handle materials (for cube, material should be an array of 6 materials)", + " if (Array.isArray(threeObject.material) && threeObject.material[materialIndex]) {", + " let material = threeObject.material[materialIndex];", + " ", + " // Convert to MeshStandardMaterial if needed", + " if (!(material instanceof THREE.MeshStandardMaterial)) {", + " const oldMap = material.map;", + " material = new THREE.MeshStandardMaterial({", + " map: oldMap,", + " roughness: 1.0,", + " metalness: 0.0", + " });", + " threeObject.material[materialIndex] = material;", + " }", + " ", + " // Create and apply the normal map", + " if (normalMapTexture) {", + " const texture = gdjs.getTexture(normalMapTexture);", + " const normalMap = new THREE.Texture(texture.getImage());", + " ", + " // Set texture parameters", + " normalMap.wrapS = THREE.RepeatWrapping;", + " normalMap.wrapT = THREE.RepeatWrapping;", + " normalMap.needsUpdate = true;", + " ", + " // Apply to material", + " material.normalMap = normalMap;", + " material.normalScale.set(", + " eventsFunctionContext.getArgument(\"NormalStrength\"),", + " eventsFunctionContext.getArgument(\"NormalStrength\")", + " );", + " material.needsUpdate = true;", + " }", + " } else {", + " console.warn('Material array or index not valid');", + " }", + " }", + "});" + ], + "parameterObjects": "ob", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Scene3D::Cube3DObject", + "type": "objectList" + }, + { + "description": "Face", + "name": "Face", + "supplementaryInformation": "[\"Front\",\"Back\",\"Top\",\"Bottom\",\"Right\",\"Left\"]", + "type": "stringWithSelector" + }, + { + "description": "Texture", + "name": "Texture", + "type": "imageResource" + }, + { + "description": "Strength of the normal texture", + "name": "NormalStrength", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Makes the object look cartoon shaded (use this with outline to make a good toon shader).", + "fullName": "Make the object toon shaded", + "functionType": "Action", + "group": "Material", + "name": "ApplyToon", + "sentence": "Make _PARAM1_'s material Toon , Color: _PARAM2_ Gradient: _PARAM3_ ", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const gradientName = eventsFunctionContext.getArgument(\"Gradiant\");", + "let gradientMap;", + "", + "// Try to load from GDevelop resources first", + "if (typeof gdjs !== 'undefined' && gdjs.getTexture) {", + " try {", + " const textureWrapper = gdjs.getTexture(gradientName);", + " if (textureWrapper) {", + " const image = textureWrapper.getImage();", + " gradientMap = new THREE.Texture(image);", + " gradientMap.needsUpdate = true;", + " }", + " } catch(e) {", + " console.warn(\"Toon Shader: Could not load texture resource\", e);", + " }", + "}", + "", + "// Fallback to basic loader if resource loading failed", + "if (!gradientMap) {", + " gradientMap = new THREE.TextureLoader().load(gradientName);", + "}", + "", + "gradientMap.minFilter = THREE.NearestFilter;", + "gradientMap.magFilter = THREE.NearestFilter;", + "gradientMap.generateMipmaps = false;", + "", + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " // Skip outline and fill meshes when applying toon material", + " if (child.userData.isToonOutline || child.userData.isToonFill) return;", + " ", + " if (child.isMesh && child.material) {", + " const oldMat = child.material;", + " ", + " // Store original material if not already stored", + " if (!child.userData.originalMaterial) {", + " child.userData.originalMaterial = oldMat;", + " }", + "", + " // Create new Toon Material", + " const newMat = new THREE.MeshToonMaterial({", + " color: gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"Color\")),", + " gradientMap: gradientMap,", + " transparent: oldMat.transparent,", + " opacity: oldMat.opacity,", + " side: oldMat.side,", + " alphaTest: oldMat.alphaTest,", + " visible: oldMat.visible,", + " wireframe: oldMat.wireframe,", + " flatShading: oldMat.flatShading", + " });", + "", + " // Preserve textures and key properties", + " if (oldMat.map) newMat.map = oldMat.map;", + " if (oldMat.alphaMap) newMat.alphaMap = oldMat.alphaMap;", + " if (oldMat.aoMap) newMat.aoMap = oldMat.aoMap;", + " if (oldMat.normalMap) {", + " newMat.normalMap = oldMat.normalMap;", + " newMat.normalScale.copy(oldMat.normalScale || new THREE.Vector2(1, 1));", + " }", + " if (oldMat.bumpMap) {", + " newMat.bumpMap = oldMat.bumpMap;", + " newMat.bumpScale = oldMat.bumpScale !== undefined ? oldMat.bumpScale : 1;", + " }", + " if (oldMat.displacementMap) {", + " newMat.displacementMap = oldMat.displacementMap;", + " newMat.displacementScale = oldMat.displacementScale !== undefined ? oldMat.displacementScale : 1;", + " newMat.displacementBias = oldMat.displacementBias !== undefined ? oldMat.displacementBias : 0;", + " }", + " if (oldMat.emissiveMap) {", + " newMat.emissiveMap = oldMat.emissiveMap;", + " newMat.emissive = oldMat.emissive;", + " newMat.emissiveIntensity = oldMat.emissiveIntensity !== undefined ? oldMat.emissiveIntensity : 1;", + " }", + "", + " // Handle Skinning (for animated models)", + " if (child.isSkinnedMesh || (child.skeleton && child.bindMatrix)) {", + " newMat.skinning = true;", + " }", + "", + " child.material = newMat;", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Scene3D::Cube3DObject", + "type": "objectList" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + }, + { + "description": "Gradient with steps (just have like 3 or 5 pixels with different brightness's)", + "name": "Gradiant", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Apply highly customizable procedural toon shading without needing a gradient image. Control the shadow/light transition mathematically.", + "fullName": "Apply Advanced/Procedural Toon Shading", + "functionType": "Action", + "group": "Material", + "name": "ApplyAdvancedToon", + "sentence": "Apply advanced toon shading to _PARAM1_ (Color: _PARAM2_, Shadow Brightness: _PARAM3_, Threshold: _PARAM4_, Softness: _PARAM5_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const colorVal = gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"Color\"));", + "const shadowBrightness = eventsFunctionContext.getArgument(\"ShadowBrightness\");", + "const threshold = eventsFunctionContext.getArgument(\"Threshold\");", + "const softness = eventsFunctionContext.getArgument(\"Softness\");", + "", + "// Generate Procedural Gradient Map", + "const size = 256;", + "const data = new Uint8Array(size * 4); // RGBA", + "", + "for (let i = 0; i < size; i++) {", + " const u = i / (size - 1); // 0.0 to 1.0", + " let mixVal = 0;", + "", + " if (softness <= 0.01) {", + " // Hard Step", + " mixVal = (u >= threshold) ? 1.0 : 0.0;", + " } else {", + " // Smooth Step", + " const t1 = threshold - (softness * 0.5);", + " const t2 = threshold + (softness * 0.5);", + " mixVal = (u - t1) / (t2 - t1);", + " mixVal = Math.min(Math.max(mixVal, 0.0), 1.0); // Clamp", + " // Cubic smoothing for better aesthetics", + " mixVal = mixVal * mixVal * (3.0 - 2.0 * mixVal);", + " }", + "", + " // Calculate final intensity: map 0..1 (mixVal) to ShadowBrightness..1.0", + " const intensity = shadowBrightness + (mixVal * (1.0 - shadowBrightness));", + " ", + " const byteVal = Math.floor(intensity * 255);", + " data[i * 4] = byteVal;", + " data[i * 4 + 1] = byteVal;", + " data[i * 4 + 2] = byteVal;", + " data[i * 4 + 3] = 255;", + "}", + "", + "const gradientMap = new THREE.DataTexture(data, size, 1, THREE.RGBAFormat);", + "gradientMap.needsUpdate = true;", + "// Use Linear for smooth softness, Nearest for hard edge (if softness is basically 0)", + "gradientMap.minFilter = (softness > 0.05) ? THREE.LinearFilter : THREE.NearestFilter;", + "gradientMap.magFilter = (softness > 0.05) ? THREE.LinearFilter : THREE.NearestFilter;", + "gradientMap.generateMipmaps = false;", + "", + "// Apply to Objects", + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " // Skip special toon meshes", + " if (child.userData.isToonOutline || child.userData.isToonFill) return;", + "", + " if (child.isMesh && child.material) {", + " const oldMat = child.material;", + " ", + " if (!child.userData.originalMaterial) {", + " child.userData.originalMaterial = oldMat;", + " }", + "", + " const newMat = new THREE.MeshToonMaterial({", + " color: colorVal,", + " gradientMap: gradientMap,", + " transparent: oldMat.transparent,", + " opacity: oldMat.opacity,", + " side: oldMat.side,", + " alphaTest: oldMat.alphaTest,", + " visible: oldMat.visible,", + " wireframe: oldMat.wireframe,", + " flatShading: oldMat.flatShading", + " });", + "", + " // Preserve Texture Maps", + " if (oldMat.map) newMat.map = oldMat.map;", + " if (oldMat.alphaMap) newMat.alphaMap = oldMat.alphaMap;", + " if (oldMat.aoMap) newMat.aoMap = oldMat.aoMap;", + " if (oldMat.normalMap) {", + " newMat.normalMap = oldMat.normalMap;", + " newMat.normalScale.copy(oldMat.normalScale || new THREE.Vector2(1, 1));", + " }", + " if (oldMat.bumpMap) {", + " newMat.bumpMap = oldMat.bumpMap;", + " newMat.bumpScale = oldMat.bumpScale;", + " }", + " if (oldMat.displacementMap) {", + " newMat.displacementMap = oldMat.displacementMap;", + " newMat.displacementScale = oldMat.displacementScale;", + " newMat.displacementBias = oldMat.displacementBias;", + " }", + " if (oldMat.emissiveMap) {", + " newMat.emissiveMap = oldMat.emissiveMap;", + " newMat.emissive = oldMat.emissive;", + " newMat.emissiveIntensity = oldMat.emissiveIntensity;", + " }", + "", + " // Handle Skinning", + " if (child.isSkinnedMesh || (child.skeleton && child.bindMatrix)) {", + " newMat.skinning = true;", + " }", + "", + " child.material = newMat;", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + }, + { + "description": "Base Color", + "name": "Color", + "type": "color" + }, + { + "description": "Shadow Brightness (0.0 = Black, 0.5 = Half Dark, 1.0 = No Shadow)", + "name": "ShadowBrightness", + "type": "expression" + }, + { + "description": "Threshold (0.5 is standard, lower = more light, higher = more shadow)", + "name": "Threshold", + "type": "expression" + }, + { + "description": "Softness (0.0 = Hard Edge, 0.1+ = Soft Transition)", + "name": "Softness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Add a glowing rim/Fresnel effect to an object's edges. Perfect for toon and stylized graphics.", + "fullName": "Apply Rim Lighting (Fresnel)", + "functionType": "Action", + "group": "Effects", + "name": "ApplyRimLight", + "sentence": "Apply rim lighting to _PARAM1_ (Color: _PARAM2_, Intensity: _PARAM3_, Spread: _PARAM4_, Sharpness: _PARAM5_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const rimColor = gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"RimColor\"));", + "const rimIntensity = eventsFunctionContext.getArgument(\"RimIntensity\");", + "const rimPower = eventsFunctionContext.getArgument(\"RimPower\");", + "const sharpness = eventsFunctionContext.getArgument(\"Sharpness\");", + "", + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " // Skip outline and fill meshes", + " if (child.userData.isToonOutline || child.userData.isToonFill) return;", + " ", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " ", + " materials.forEach(material => {", + " // Skip if this material is marked as outline/fill", + " if (material.userData.isToonOutline || material.userData.isToonFill) return;", + " ", + " // Support all materials that have onBeforeCompile", + " if (material.onBeforeCompile !== undefined) {", + " ", + " if (material.userData.rimUniforms) {", + " // Update existing uniforms", + " material.userData.rimUniforms.rimColor.value.setHex(rimColor);", + " material.userData.rimUniforms.rimIntensity.value = rimIntensity;", + " material.userData.rimUniforms.rimPower.value = rimPower;", + " material.userData.rimUniforms.rimSharpness.value = sharpness;", + " material.userData.rimUniforms.rimOpacity.value = eventsFunctionContext.getArgument(\"Opacity\") !== undefined ? eventsFunctionContext.getArgument(\"Opacity\") : 1.0;", + " material.needsUpdate = true;", + " } else {", + " // Initialize uniforms", + " material.userData.rimUniforms = {", + " rimColor: { value: new THREE.Color(rimColor) },", + " rimIntensity: { value: rimIntensity },", + " rimPower: { value: rimPower },", + " rimSharpness: { value: sharpness },", + " rimOpacity: { value: eventsFunctionContext.getArgument(\"Opacity\") !== undefined ? eventsFunctionContext.getArgument(\"Opacity\") : 1.0 }", + " };", + "", + " const oldOnBeforeCompile = material.onBeforeCompile;", + " material.onBeforeCompile = (shader) => {", + " if (oldOnBeforeCompile) oldOnBeforeCompile(shader);", + "", + " // Add uniforms to the shader", + " Object.assign(shader.uniforms, material.userData.rimUniforms);", + "", + " // CUSTOM VARS to avoid conflicts with Standard Materials", + " const isBasic = material.type === 'MeshBasicMaterial';", + " ", + " // 1. Vertex Shader Injection", + " if (!shader.vertexShader.includes('varying vec3 vToonNormal')) {", + " let vertexDefs = 'varying vec3 vToonNormal;\\nvarying vec3 vToonViewPos;\\n';", + " if (isBasic && !shader.vertexShader.includes('attribute vec3 normal')) {", + " vertexDefs = 'attribute vec3 normal;\\n' + vertexDefs;", + " }", + " shader.vertexShader = shader.vertexShader.replace(", + " '#include ',", + " '#include \\n' + vertexDefs", + " );", + " shader.vertexShader = shader.vertexShader.replace(", + " '#include ', ", + " '#include \\nvToonNormal = normalize(normalMatrix * normal);\\nvToonViewPos = -mvPosition.xyz;'", + " );", + " }", + "", + " // 2. Fragment Shader Injection", + " if (!shader.fragmentShader.includes('varying vec3 vToonNormal')) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " '#include ',", + " '#include \\nvarying vec3 vToonNormal;\\nvarying vec3 vToonViewPos;\\n'", + " );", + " }", + "", + " // Inject uniforms into header", + " shader.fragmentShader = `", + " uniform vec3 rimColor;", + " uniform float rimIntensity;", + " uniform float rimPower;", + " uniform float rimSharpness;", + " uniform float rimOpacity;", + " ` + shader.fragmentShader;", + "", + " // Inject calculation logic", + " shader.fragmentShader = shader.fragmentShader.replace(", + " '#include ',", + " `", + " #include ", + " ", + " // Fresnel/Rim Calculation", + " vec3 rim_n = normalize(vToonNormal + 0.00001);", + " vec3 rim_v = normalize(-vToonViewPos + 0.00001);", + " float rim = 1.0 - max(dot(rim_n, rim_v), 0.0);", + " rim = pow(rim, rimPower);", + " ", + " // Apply Sharpness (Toon Step)", + " if (rimSharpness > 0.0) {", + " float edge = (1.0001 - rimSharpness) * 0.5; ", + " rim = smoothstep(0.5 - edge, 0.5 + edge, rim);", + " }", + " ", + " gl_FragColor.rgb = mix(gl_FragColor.rgb, gl_FragColor.rgb + (rimColor * rimIntensity), clamp(rim * rimOpacity, 0.0, 1.0));", + " `", + " );", + " };", + " material.needsUpdate = true;", + " }", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + }, + { + "description": "Rim Color", + "name": "RimColor", + "type": "color" + }, + { + "description": "Intensity (Brightness - try values like 1-5)", + "name": "RimIntensity", + "type": "expression" + }, + { + "description": "Spread (Lower is wider, higher is thinner - try values like 2-10)", + "name": "RimPower", + "type": "expression" + }, + { + "description": "Sharpness (0 for soft, 1 for hard toon edge)", + "name": "Sharpness", + "type": "expression" + }, + { + "description": "Opacity (0.0 to 1.0)", + "name": "Opacity", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Add sharp cartoonish specular highlights (shiny spots) to an object. Requires a light source in the scene.", + "fullName": "Apply Cartoon Specular", + "functionType": "Action", + "group": "Effects", + "name": "ApplyCartoonSpecular", + "sentence": "Apply specular highlights to _PARAM1_ (Color: _PARAM2_, Size: _PARAM3_, Sharpness: _PARAM4_ LightDir: _PARAM5_, _PARAM6_, _PARAM7_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const specColor = gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"Color\"));", + "const specSize = eventsFunctionContext.getArgument(\"Size\");", + "const sharpness = eventsFunctionContext.getArgument(\"Sharpness\");", + "// Default to a professional Studio Light setup (Top-Right-Front)", + "const lightX = eventsFunctionContext.getArgument(\"LightX\") || 0.5;", + "const lightY = eventsFunctionContext.getArgument(\"LightY\") || 0.5;", + "const lightZ = eventsFunctionContext.getArgument(\"LightZ\") || 0.5;", + "", + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " if (child.userData.isToonOutline || child.userData.isToonFill) return;", + " ", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " ", + " materials.forEach(material => {", + " if (material.userData.isToonOutline || material.userData.isToonFill) return;", + " ", + " // Ensure we have a valid material for injection", + " if (material.onBeforeCompile !== undefined) {", + " if (material.userData.specUniforms) {", + " // Update existing uniforms", + " material.userData.specUniforms.specColor.value.setHex(specColor);", + " material.userData.specUniforms.specSize.value = specSize;", + " material.userData.specUniforms.specSharpness.value = sharpness;", + " material.userData.specUniforms.specOpacity.value = eventsFunctionContext.getArgument(\"Opacity\") !== undefined ? eventsFunctionContext.getArgument(\"Opacity\") : 1.0;", + " material.userData.specUniforms.specLightDir.value.set(lightX, lightY, lightZ).normalize();", + " material.needsUpdate = true;", + " } else {", + " // Initialize uniforms", + " material.userData.specUniforms = {", + " specColor: { value: new THREE.Color(specColor) },", + " specSize: { value: specSize },", + " specSharpness: { value: sharpness },", + " specOpacity: { value: eventsFunctionContext.getArgument(\"Opacity\") !== undefined ? eventsFunctionContext.getArgument(\"Opacity\") : 1.0 },", + " specLightDir: { value: new THREE.Vector3(lightX, lightY, lightZ).normalize() }", + " };", + "", + " const oldOnBeforeCompile = material.onBeforeCompile;", + " material.onBeforeCompile = (shader) => {", + " if (oldOnBeforeCompile) oldOnBeforeCompile(shader);", + " Object.assign(shader.uniforms, material.userData.specUniforms);", + "", + " const isBasic = material.type === 'MeshBasicMaterial';", + " ", + " // --- SAFELY INJECT DEFINITIONS ---", + " // CRITICAL FIX: Inject AFTER to avoid breaking WebGL2 version/precision headers.", + " // If we prepend to start of string, we break the shader.", + " ", + " const vertexDefs = (isBasic && !shader.vertexShader.includes('attribute vec3 normal')) ? ", + " `attribute vec3 normal;\\nvarying vec3 vToonNormal;\\nvarying vec3 vToonViewPos;` : ", + " `varying vec3 vToonNormal;\\nvarying vec3 vToonViewPos;`;", + " ", + " const fragmentDefs = `", + " varying vec3 vToonNormal;", + " varying vec3 vToonViewPos;", + " uniform vec3 specColor;", + " uniform float specSize;", + " uniform float specSharpness;", + " uniform float specOpacity;", + " uniform vec3 specLightDir;", + " `;", + "", + " // 1. Vertex Shader Injection", + " if (!shader.vertexShader.includes('varying vec3 vToonNormal')) {", + " shader.vertexShader = shader.vertexShader.replace(", + " '#include ',", + " '#include \\n' + vertexDefs", + " );", + " ", + " // Inject logic", + " shader.vertexShader = shader.vertexShader.replace(", + " '#include ',", + " `", + " #include ", + " ", + " // Robust Normal Calculation", + " vToonNormal = normalize(normalMatrix * normal);", + " vToonViewPos = -mvPosition.xyz;", + " `", + " );", + " }", + "", + " // 2. Fragment Shader Injection", + " if (!shader.fragmentShader.includes('uniform vec3 specColor')) {", + " shader.fragmentShader = shader.fragmentShader.replace(", + " '#include ',", + " '#include \\n' + fragmentDefs", + " );", + " }", + "", + " // 3. Main Specular Logic", + " shader.fragmentShader = shader.fragmentShader.replace(", + " '#include ',", + " `", + " #include ", + " ", + " // Professional Blinn-Phong Specular Calculation", + " vec3 normal = normalize(vToonNormal + 0.00001);", + " vec3 viewDir = normalize(vToonViewPos + 0.00001);", + " vec3 lightDir = normalize(specLightDir);", + " ", + " // Prevent 'Glowing Butt' - only apply specular on lit side", + " float NdotL = dot(normal, lightDir);", + " ", + " if (NdotL > 0.0) {", + " vec3 halfVector = normalize(lightDir + viewDir);", + " float NdotH = max(0.0, dot(normal, halfVector));", + " ", + " // Convert Size to Shininess Power (Robust Math)", + " float shininess = 10.0 / max(specSize, 0.001);", + " float specular = pow(NdotH, shininess);", + " ", + " // Toonify (Step Function)", + " if (specSharpness > 0.0) {", + " float smoothRange = 0.02 * (1.0 - specSharpness);", + " float threshold = 0.5; ", + " specular = smoothstep(threshold - smoothRange, threshold + smoothRange, specular);", + " }", + " ", + " gl_FragColor.rgb = mix(gl_FragColor.rgb, gl_FragColor.rgb + specColor, clamp(specular * specOpacity, 0.0, 1.0));", + " }", + " `", + " );", + " };", + " material.needsUpdate = true;", + " }", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + }, + { + "description": "Highlight Color", + "name": "Color", + "type": "color" + }, + { + "description": "Size (Higher values make it smaller/sharper - try 5 to 50)", + "name": "Size", + "type": "expression" + }, + { + "description": "Sharpness (0 for soft, 1 for hard cartoon dot)", + "name": "Sharpness", + "type": "expression" + }, + { + "description": "Light X Direction (e.g. 0.5)", + "name": "LightX", + "type": "expression" + }, + { + "description": "Light Y Direction (e.g. 0.8)", + "name": "LightY", + "type": "expression" + }, + { + "description": "Light Z Direction (e.g. 0.5)", + "name": "LightZ", + "type": "expression" + }, + { + "description": "Opacity (0.0 to 1.0)", + "name": "Opacity", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove specular highlights from an object.", + "fullName": "Remove Specular Highlights", + "functionType": "Action", + "group": "Effects", + "name": "RemoveCartoonSpecular", + "sentence": "Remove specular highlights from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " materials.forEach(material => {", + " if (material.userData.specUniforms) {", + " material.userData.specUniforms.specColor.value.setRGB(0,0,0);", + " material.needsUpdate = true;", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "Remove the rim lighting effect from an object.", + "fullName": "Remove Rim Lighting", + "functionType": "Action", + "group": "Effects", + "name": "RemoveRimLight", + "sentence": "Remove rim lighting from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " materials.forEach(material => {", + " if (material.userData.rimUniforms) {", + " material.userData.rimUniforms.rimIntensity.value = 0;", + " material.needsUpdate = true;", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "The object will appear white if its close to the near plane, and black if its close to the far plane.", + "fullName": "Make the object Depth shaded", + "functionType": "Action", + "group": "Material", + "name": "MakeDepth", + "sentence": "Make _PARAM1_'s material Depth, Scale: _PARAM2_ Offset: _PARAM3_ ", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "objects.forEach(object => {", + " const threeObject = object.get3DRendererObject();", + " if (threeObject) {", + " threeObject.material = new THREE.MeshDepthMaterial({", + " displacementScale: eventsFunctionContext.getArgument(\"Scale\"),", + " displacementBias: eventsFunctionContext.getArgument(\"Offset\")", + " });", + " }", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Scene3D::Cube3DObject", + "type": "objectList" + }, + { + "description": "Scale", + "name": "Scale", + "type": "expression" + }, + { + "description": "Offset", + "name": "Offset", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Add or change an outline of a 3D object using backface rendering (FIXED VERSION).", + "fullName": "Add/Change Outline", + "functionType": "Action", + "group": "Effects", + "name": "AddCartoonOutline", + "sentence": "Add cartoon outline to _PARAM1_ (Fill: _PARAM2_, Outline: _PARAM3_, Fill Opacity: _PARAM4_, Outline Opacity: _PARAM5_, Thickness: _PARAM6_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "objects.forEach(object => {", + " const threeObject = object.get3DRendererObject();", + " ", + " if (threeObject) {", + " // Get parameters", + " const fillColor = gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"FillColor\"));", + " const outlineColor = gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"OutlineColor\"));", + " const fillOpacity = eventsFunctionContext.getArgument(\"FillOpacity\");", + " const outlineOpacity = eventsFunctionContext.getArgument(\"OutlineOpacity\");", + " const outlineThickness = eventsFunctionContext.getArgument(\"OutlineThickness\");", + " const fillRenderOrder = eventsFunctionContext.getArgument(\"FillRenderOrder\");", + " const outlineRenderOrder = eventsFunctionContext.getArgument(\"OutlineRenderOrder\");", + "", + " // Find all meshes in the object (handles both single meshes and groups/models)", + " const meshes = [];", + " threeObject.traverse((child) => {", + " // CRITICAL FIX: Skip outline and fill meshes we already created", + " if (child.userData.isToonOutline || child.userData.isToonFill) return;", + " ", + " // Only target THREE.Mesh with valid geometry", + " if (child instanceof THREE.Mesh && child.geometry) {", + " meshes.push(child);", + " }", + " });", + "", + " if (outlineThickness > 0 && meshes.length > 0) {", + " ", + " // --- Creation Logic (Runs only once) ---", + " if (!threeObject.userData.fillOverlays) {", + " threeObject.userData.fillOverlays = [];", + " threeObject.userData.outlineMeshes = [];", + " threeObject.userData.originalMeshes = []; // Store references", + "", + " meshes.forEach((mesh) => {", + " const isAnimated = eventsFunctionContext.getArgument(\"IsAnimated\");", + " const isSkinned = isAnimated || mesh.isSkinnedMesh || (mesh.skeleton && mesh.bindMatrix);", + " ", + " // Store reference to original mesh", + " threeObject.userData.originalMeshes.push(mesh);", + " ", + " // 1. Create Fill Overlay", + " const fillMaterial = new THREE.MeshBasicMaterial({", + " color: fillColor,", + " transparent: true,", + " opacity: fillOpacity,", + " side: THREE.FrontSide,", + " depthTest: true,", + " depthWrite: true", + " });", + " // Mark as toon fill to prevent rim/specular effects", + " fillMaterial.userData.isToonFill = true;", + "", + " const fillGeometry = mesh.geometry.clone();", + " let fillOverlay;", + " ", + " // Create correct mesh type based on animation", + " if (isSkinned && mesh.isSkinnedMesh) {", + " fillOverlay = new THREE.SkinnedMesh(fillGeometry, fillMaterial);", + " fillOverlay.bind(mesh.skeleton, mesh.bindMatrix);", + " } else {", + " fillOverlay = new THREE.Mesh(fillGeometry, fillMaterial);", + " }", + " ", + " // Mark mesh as toon fill", + " fillOverlay.userData.isToonFill = true;", + " fillOverlay.renderOrder = fillRenderOrder;", + " fillOverlay.frustumCulled = mesh.frustumCulled;", + " ", + " // Copy transformation from original mesh", + " fillOverlay.position.copy(mesh.position);", + " fillOverlay.rotation.copy(mesh.rotation);", + " fillOverlay.scale.copy(mesh.scale);", + " ", + " mesh.parent.add(fillOverlay);", + " threeObject.userData.fillOverlays.push(fillOverlay);", + "", + " // 2. Create Outline using BACKFACE rendering", + " const outlineMaterial = new THREE.MeshBasicMaterial({", + " color: outlineColor,", + " transparent: true,", + " opacity: outlineOpacity,", + " side: THREE.BackSide,", + " depthTest: true,", + " depthWrite: false, // Allow objects behind to show through", + " polygonOffset: true, // Prevent z-fighting", + " polygonOffsetFactor: 1,", + " polygonOffsetUnits: 1", + " });", + " // Mark as toon outline to prevent rim/specular effects", + " outlineMaterial.userData.isToonOutline = true;", + "", + " const outlineGeometry = mesh.geometry.clone();", + " let outlineMesh;", + " ", + " // Create correct mesh type based on animation", + " if (isSkinned && mesh.isSkinnedMesh) {", + " outlineMesh = new THREE.SkinnedMesh(outlineGeometry, outlineMaterial);", + " outlineMesh.bind(mesh.skeleton, mesh.bindMatrix);", + " } else {", + " outlineMesh = new THREE.Mesh(outlineGeometry, outlineMaterial);", + " }", + " ", + " // Mark mesh as toon outline", + " outlineMesh.userData.isToonOutline = true;", + " outlineMesh.renderOrder = outlineRenderOrder - 1;", + " outlineMesh.frustumCulled = mesh.frustumCulled;", + " ", + " // Copy transformation from original mesh", + " outlineMesh.position.copy(mesh.position);", + " outlineMesh.rotation.copy(mesh.rotation);", + " // Apply thickness scaling", + " const initScaleFactor = 1.0 + (outlineThickness * 0.01);", + " outlineMesh.scale.set(", + " mesh.scale.x * initScaleFactor,", + " mesh.scale.y * initScaleFactor,", + " mesh.scale.z * initScaleFactor", + " );", + " ", + " mesh.parent.add(outlineMesh);", + " threeObject.userData.outlineMeshes.push(outlineMesh);", + " });", + " }", + "", + " // --- Update Logic (Runs every frame) ---", + " // Use stored originalMeshes to avoid re-traversing", + " const storedMeshes = threeObject.userData.originalMeshes || [];", + " storedMeshes.forEach((mesh, index) => {", + " // Update Fill Overlay", + " if (threeObject.userData.fillOverlays[index]) {", + " const fillOverlay = threeObject.userData.fillOverlays[index];", + " fillOverlay.material.color.setHex(fillColor);", + " fillOverlay.material.opacity = fillOpacity;", + " fillOverlay.renderOrder = fillRenderOrder;", + " ", + " // Ensure position/rotation/scale matches the original mesh", + " fillOverlay.position.copy(mesh.position);", + " fillOverlay.rotation.copy(mesh.rotation);", + " fillOverlay.scale.copy(mesh.scale);", + " fillOverlay.visible = true;", + " ", + " if (mesh.skeleton && fillOverlay.skeleton) {", + " fillOverlay.skeleton = mesh.skeleton;", + " }", + " }", + "", + " // Update Outline with proper scaling based on thickness", + " if (threeObject.userData.outlineMeshes[index]) {", + " const outlineMesh = threeObject.userData.outlineMeshes[index];", + " ", + " // Update material properties", + " outlineMesh.material.color.setHex(outlineColor);", + " outlineMesh.material.opacity = outlineOpacity;", + " outlineMesh.renderOrder = outlineRenderOrder - 1;", + " ", + " // Position and rotation match the mesh", + " outlineMesh.position.copy(mesh.position);", + " outlineMesh.rotation.copy(mesh.rotation);", + " ", + " // CRUCIAL: Scale the outline mesh to be larger based on thickness", + " const scaleFactor = 1 + (outlineThickness * 0.01);", + " outlineMesh.scale.set(", + " mesh.scale.x * scaleFactor,", + " mesh.scale.y * scaleFactor,", + " mesh.scale.z * scaleFactor", + " );", + " ", + " outlineMesh.visible = true;", + " ", + " if (mesh.skeleton && outlineMesh.skeleton) {", + " outlineMesh.skeleton = mesh.skeleton;", + " }", + " }", + " });", + "", + " } else {", + " // Hide overlay effects if thickness is 0 or less", + " if (threeObject.userData.fillOverlays) {", + " threeObject.userData.fillOverlays.forEach(overlay => {", + " overlay.visible = false;", + " });", + " }", + " if (threeObject.userData.outlineMeshes) {", + " threeObject.userData.outlineMeshes.forEach(mesh => {", + " mesh.visible = false;", + " });", + " }", + " }", + " }", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + }, + { + "description": "Fill", + "name": "FillColor", + "type": "color" + }, + { + "description": "Outline", + "name": "OutlineColor", + "type": "color" + }, + { + "description": "Fill Opacity", + "name": "FillOpacity", + "type": "expression" + }, + { + "description": "Outline Opacity", + "name": "OutlineOpacity", + "type": "expression" + }, + { + "description": "Thickness of the outline (use higher values like 5-20 for visible effect)", + "name": "OutlineThickness", + "type": "expression" + }, + { + "description": "Fill Render Order", + "name": "FillRenderOrder", + "type": "expression" + }, + { + "description": "Outline Render Order", + "name": "OutlineRenderOrder", + "type": "expression" + }, + { + "description": "Is this an animated (Skinned) object? (True for models with bones)", + "name": "IsAnimated", + "type": "yesno" + } + ], + "objectGroups": [] + }, + { + "description": "Remove cartoon outline from an object.", + "fullName": "Remove outline", + "functionType": "Action", + "group": "Effects", + "name": "RemoveCartoonOutline", + "sentence": "Remove cartoon outline from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "objects.forEach(object => {", + " const threeObject = object.get3DRendererObject();", + " ", + " if (threeObject) {", + " // Remove Fill Overlays", + " if (threeObject.userData.fillOverlays && Array.isArray(threeObject.userData.fillOverlays)) {", + " threeObject.userData.fillOverlays.forEach(overlay => {", + " if (overlay.parent) overlay.parent.remove(overlay);", + " if (overlay.geometry) overlay.geometry.dispose();", + " if (overlay.material) overlay.material.dispose();", + " });", + " threeObject.userData.fillOverlays = [];", + " }", + " ", + " // Remove Outline Meshes", + " if (threeObject.userData.outlineMeshes && Array.isArray(threeObject.userData.outlineMeshes)) {", + " threeObject.userData.outlineMeshes.forEach(mesh => {", + " if (mesh.parent) mesh.parent.remove(mesh);", + " if (mesh.geometry) mesh.geometry.dispose();", + " if (mesh.material) mesh.material.dispose();", + " });", + " threeObject.userData.outlineMeshes = [];", + " }", + " ", + " // Clear stored original meshes references", + " if (threeObject.userData.originalMeshes) {", + " threeObject.userData.originalMeshes = [];", + " }", + " ", + " // Check for old legacy userData (just in case)", + " if (threeObject.userData.fillOverlay) {", + " if (threeObject.userData.fillOverlay.parent) threeObject.userData.fillOverlay.parent.remove(threeObject.userData.fillOverlay);", + " threeObject.userData.fillOverlay = undefined;", + " }", + " if (threeObject.userData.outlineMesh) {", + " if (threeObject.userData.outlineMesh.parent) threeObject.userData.outlineMesh.parent.remove(threeObject.userData.outlineMesh);", + " threeObject.userData.outlineMesh = undefined;", + " }", + "", + " // Reset render order is tricky with multiple meshes, but we can reset the root", + " threeObject.renderOrder = 0;", + " }", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": false + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "supplementaryInformation": "Scene3D::Cube3DObject", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "Apply toon shading to all objects on a specific layer.", + "fullName": "Apply Toon Shading to Layer", + "functionType": "Action", + "group": "Layer Effects", + "name": "ApplyToonToLayer", + "sentence": "Apply toon shading to all objects on layer _PARAM1_ (Color: _PARAM2_, Gradient: _PARAM3_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const layerName = eventsFunctionContext.getArgument(\"Layer\");", + "const color = eventsFunctionContext.getArgument(\"Color\");", + "const gradient = eventsFunctionContext.getArgument(\"Gradiant\");", + "", + "const instances = [];", + "const objectNames = runtimeScene.getObjects().map(o => o.getName());", + "objectNames.forEach(name => {", + " const list = runtimeScene.getInstancesOfObject(name);", + " for (let i = 0; i < list.length; i++) {", + " if (list[i].getLayer() === layerName) {", + " instances.push(list[i]);", + " }", + " }", + "});", + "", + "if (instances.length > 0) {", + " eventsFunctionContext.getEventsFunctionContext()._extToonShaderApplyToon(instances, color, gradient);", + "}" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Layer name", + "name": "Layer", + "type": "layer" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + }, + { + "description": "Gradient Resource", + "name": "Gradiant", + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Apply rim lighting to all objects on a specific layer.", + "fullName": "Apply Rim Lighting to Layer", + "functionType": "Action", + "group": "Layer Effects", + "name": "ApplyRimToLayer", + "sentence": "Apply rim lighting to all objects on layer _PARAM1_ (Color: _PARAM2_, Intensity: _PARAM3_, Spread: _PARAM4_, Sharpness: _PARAM5_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const layerName = eventsFunctionContext.getArgument(\"Layer\");", + "const rimColor = eventsFunctionContext.getArgument(\"RimColor\");", + "const rimIntensity = eventsFunctionContext.getArgument(\"RimIntensity\");", + "const rimPower = eventsFunctionContext.getArgument(\"RimPower\");", + "const sharpness = eventsFunctionContext.getArgument(\"Sharpness\");", + "const opacity = eventsFunctionContext.getArgument(\"Opacity\");", + "", + "const instances = [];", + "const objectNames = runtimeScene.getObjects().map(o => o.getName());", + "objectNames.forEach(name => {", + " const list = runtimeScene.getInstancesOfObject(name);", + " for (let i = 0; i < list.length; i++) {", + " if (list[i].getLayer() === layerName) {", + " instances.push(list[i]);", + " }", + " }", + "});", + "", + "if (instances.length > 0) {", + " eventsFunctionContext.getEventsFunctionContext()._extToonShaderApplyRimLight(instances, rimColor, rimIntensity, rimPower, sharpness, opacity);", + "}" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Layer name", + "name": "Layer", + "type": "layer" + }, + { + "description": "Rim Color", + "name": "RimColor", + "type": "color" + }, + { + "description": "Intensity", + "name": "RimIntensity", + "type": "expression" + }, + { + "description": "Spread", + "name": "RimPower", + "type": "expression" + }, + { + "description": "Sharpness", + "name": "Sharpness", + "type": "expression" + }, + { + "description": "Opacity", + "name": "Opacity", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Add cartoon outline to all objects on a specific layer.", + "fullName": "Add Outline to Layer", + "functionType": "Action", + "group": "Layer Effects", + "name": "AddOutlineToLayer", + "sentence": "Add/Change outline for all objects on layer _PARAM1_ (Fill: _PARAM2_, Outline: _PARAM3_, Fill Opacity: _PARAM4_, Outline Opacity: _PARAM5_, Thickness: _PARAM6_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const layerName = eventsFunctionContext.getArgument(\"Layer\");", + "const fillColor = eventsFunctionContext.getArgument(\"FillColor\");", + "const outlineColor = eventsFunctionContext.getArgument(\"OutlineColor\");", + "const fillOpacity = eventsFunctionContext.getArgument(\"FillOpacity\");", + "const outlineOpacity = eventsFunctionContext.getArgument(\"OutlineOpacity\");", + "const thickness = eventsFunctionContext.getArgument(\"OutlineThickness\");", + "const fillOrder = eventsFunctionContext.getArgument(\"FillRenderOrder\");", + "const outlineOrder = eventsFunctionContext.getArgument(\"OutlineRenderOrder\");", + "const isAnimated = eventsFunctionContext.getArgument(\"IsAnimated\");", + "", + "const instances = [];", + "const objectNames = runtimeScene.getObjects().map(o => o.getName());", + "objectNames.forEach(name => {", + " const list = runtimeScene.getInstancesOfObject(name);", + " for (let i = 0; i < list.length; i++) {", + " if (list[i].getLayer() === layerName) {", + " instances.push(list[i]);", + " }", + " }", + "});", + "", + "if (instances.length > 0) {", + " eventsFunctionContext.getEventsFunctionContext()._extToonShaderAddCartoonOutline(instances, fillColor, outlineColor, fillOpacity, outlineOpacity, thickness, fillOrder, outlineOrder, isAnimated);", + "}" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Layer name", + "name": "Layer", + "type": "layer" + }, + { + "description": "Fill", + "name": "FillColor", + "type": "color" + }, + { + "description": "Outline", + "name": "OutlineColor", + "type": "color" + }, + { + "description": "Fill Opacity", + "name": "FillOpacity", + "type": "expression" + }, + { + "description": "Outline Opacity", + "name": "OutlineOpacity", + "type": "expression" + }, + { + "description": "Thickness", + "name": "OutlineThickness", + "type": "expression" + }, + { + "description": "Fill Render Order", + "name": "FillRenderOrder", + "type": "expression" + }, + { + "description": "Outline Render Order", + "name": "OutlineRenderOrder", + "type": "expression" + }, + { + "description": "Is this an animated (Skinned) object?", + "name": "IsAnimated", + "type": "yesno" + } + ], + "objectGroups": [] + }, + { + "description": "Apply specular highlights to all objects on a specific layer.", + "fullName": "Apply Specular Highlights to Layer", + "functionType": "Action", + "group": "Layer Effects", + "name": "ApplySpecularToLayer", + "sentence": "Apply specular highlights to all objects on layer _PARAM1_ (Color: _PARAM2_, Size: _PARAM3_, Sharpness: _PARAM4_ LightDir: _PARAM5_, _PARAM6_, _PARAM7_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const layerName = eventsFunctionContext.getArgument(\"Layer\");", + "const specColor = eventsFunctionContext.getArgument(\"Color\");", + "const specSize = eventsFunctionContext.getArgument(\"Size\");", + "const sharpness = eventsFunctionContext.getArgument(\"Sharpness\");", + "const lightX = eventsFunctionContext.getArgument(\"LightX\") || 0.5;", + "const lightY = eventsFunctionContext.getArgument(\"LightY\") || 0.8;", + "const lightZ = eventsFunctionContext.getArgument(\"LightZ\") || 0.5;", + "const opacity = eventsFunctionContext.getArgument(\"Opacity\");", + "", + "const instances = [];", + "const objectNames = runtimeScene.getObjects().map(o => o.getName());", + "objectNames.forEach(name => {", + " const list = runtimeScene.getInstancesOfObject(name);", + " for (let i = 0; i < list.length; i++) {", + " if (list[i].getLayer() === layerName) {", + " instances.push(list[i]);", + " }", + " }", + "});", + "", + "if (instances.length > 0) {", + " eventsFunctionContext.getEventsFunctionContext()._extToonShaderApplyCartoonSpecular(instances, specColor, specSize, sharpness, lightX, lightY, lightZ, opacity);", + "}" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Layer name", + "name": "Layer", + "type": "layer" + }, + { + "description": "Color", + "name": "Color", + "type": "color" + }, + { + "description": "Size", + "name": "Size", + "type": "expression" + }, + { + "description": "Sharpness", + "name": "Sharpness", + "type": "expression" + }, + { + "description": "Light X", + "name": "LightX", + "type": "expression" + }, + { + "description": "Light Y", + "name": "LightY", + "type": "expression" + }, + { + "description": "Light Z", + "name": "LightZ", + "type": "expression" + }, + { + "description": "Opacity", + "name": "Opacity", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove cartoon outline from all objects on a specific layer.", + "fullName": "Remove Outline from Layer", + "functionType": "Action", + "group": "Layer Effects", + "name": "RemoveOutlineFromLayer", + "sentence": "Remove outline from all objects on layer _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const layerName = eventsFunctionContext.getArgument(\"Layer\");", + "", + "const instances = [];", + "const objectNames = runtimeScene.getObjects().map(o => o.getName());", + "objectNames.forEach(name => {", + " const list = runtimeScene.getInstancesOfObject(name);", + " for (let i = 0; i < list.length; i++) {", + " if (list[i].getLayer() === layerName) {", + " instances.push(list[i]);", + " }", + " }", + "});", + "", + "if (instances.length > 0) {", + " eventsFunctionContext.getEventsFunctionContext()._extToonShaderRemoveCartoonOutline(instances);", + "}" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Layer name", + "name": "Layer", + "type": "layer" + } + ], + "objectGroups": [] + }, + { + "description": "Remove ALL toon effects (Toon Shading, Outline, Rim Light, Specular) from an object. Restores original appearance.", + "fullName": "Remove ALL Toon Effects", + "functionType": "Action", + "group": "Effects", + "name": "RemoveAllToonEffects", + "sentence": "Remove all toon effects from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const objects = eventsFunctionContext.getObjects(\"Object\");", + "const context = eventsFunctionContext.getEventsFunctionContext();", + "", + "if (objects.length > 0) {", + " // 1. Remove Outline", + " context._extToonShaderRemoveCartoonOutline(objects);", + " ", + " // 2. Remove Rim Light (if implemented via separate action)", + " context._extToonShaderRemoveRimLight(objects);", + " ", + " // 3. Remove Specular", + " if (context._extToonShaderRemoveCartoonSpecular) context._extToonShaderRemoveCartoonSpecular(objects);", + " ", + " // 4. Remove Emission", + " if (context._extToonShaderRemoveEmission) context._extToonShaderRemoveEmission(objects);", + " ", + " // 5. Remove Shadow Tint", + " if (context._extToonShaderRemoveShadowTint) context._extToonShaderRemoveShadowTint(objects);", + " ", + " // 6. Restore original materials for Toon Shading", + " objects.forEach(object => {", + " const root = object.get3DRendererObject();", + " if (root) {", + " root.traverse(child => {", + " if (child.isMesh && child.userData.originalMaterial) {", + " child.material = child.userData.originalMaterial;", + " delete child.userData.originalMaterial;", + " }", + " });", + " }", + " });", + "}" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "Make the object glow by setting its emissive color and intensity. Perfect for eyes, magic, or lights.", + "fullName": "Apply Emission (Glow)", + "functionType": "Action", + "group": "Effects", + "name": "ApplyEmission", + "sentence": "Apply emission to _PARAM1_ (Color: _PARAM2_, Intensity: _PARAM3_, Texture: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const color = gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"Color\"));", + "const intensity = eventsFunctionContext.getArgument(\"Intensity\");", + "const textureName = eventsFunctionContext.getArgument(\"Texture\");", + "", + "let emissiveMap = null;", + "if (textureName) {", + " const textureWrapper = gdjs.getTexture(textureName);", + " if (textureWrapper) {", + " emissiveMap = new THREE.Texture(textureWrapper.getImage());", + " emissiveMap.needsUpdate = true;", + " emissiveMap.flipY = false; // Usually GDevelop textures are flipped", + " }", + "}", + "", + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " if (child.userData.isToonOutline || child.userData.isToonFill) return;", + " ", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " materials.forEach(material => {", + " if (material.emissive !== undefined) {", + " material.emissive.setHex(color);", + " material.emissiveIntensity = intensity;", + " if (emissiveMap) {", + " material.emissiveMap = emissiveMap;", + " }", + " material.needsUpdate = true;", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + }, + { + "description": "Emission Color", + "name": "Color", + "type": "color" + }, + { + "description": "Intensity (Try 1.0 - 5.0)", + "name": "Intensity", + "type": "expression" + }, + { + "description": "Emission Texture (Optional - for masking eyes etc.)", + "name": "Texture", + "optional": true, + "type": "imageResource" + } + ], + "objectGroups": [] + }, + { + "description": "Remove emission/glow from an object.", + "fullName": "Remove Emission", + "functionType": "Action", + "group": "Effects", + "name": "RemoveEmission", + "sentence": "Remove emission from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " materials.forEach(material => {", + " if (material.emissive !== undefined) {", + " material.emissive.setHex(0x000000);", + " material.emissiveIntensity = 1;", + " material.emissiveMap = null;", + " material.needsUpdate = true;", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "Apply a Manga/Comic style halftone effect (dots) to the shadws.", + "fullName": "Apply Manga Halftone", + "functionType": "Action", + "group": "Effects", + "name": "ApplyHalftone", + "sentence": "Apply manga halftone to _PARAM1_ (Color: _PARAM2_, Scale: _PARAM3_, Intensity: _PARAM4_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const htColor = gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"Color\"));", + "const htScale = eventsFunctionContext.getArgument(\"Scale\");", + "const htIntensity = eventsFunctionContext.getArgument(\"Intensity\");", + "", + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " if (child.userData.isToonOutline || child.userData.isToonFill) return;", + " ", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " materials.forEach(material => {", + " // Support all materials", + " if (material.onBeforeCompile !== undefined) {", + " if (material.userData.htUniforms) {", + " material.userData.htUniforms.htColor.value.setHex(htColor);", + " material.userData.htUniforms.htScale.value = htScale;", + " material.userData.htUniforms.htIntensity.value = htIntensity;", + " material.needsUpdate = true;", + " } else {", + " material.userData.htUniforms = {", + " htColor: { value: new THREE.Color(htColor) },", + " htScale: { value: htScale },", + " htIntensity: { value: htIntensity }", + " };", + "", + " const oldOnBeforeCompile = material.onBeforeCompile;", + " material.onBeforeCompile = (shader) => {", + " if (oldOnBeforeCompile) oldOnBeforeCompile(shader);", + " Object.assign(shader.uniforms, material.userData.htUniforms);", + "", + " // Inject Uniforms", + " shader.fragmentShader = `", + " uniform vec3 htColor;", + " uniform float htScale;", + " uniform float htIntensity;", + " ` + shader.fragmentShader;", + "", + " // Inject Halftone Logic at the end", + " shader.fragmentShader = shader.fragmentShader.replace(", + " '#include ',", + " `", + " #include ", + " ", + " // Calculate Luminance of the current pixel color", + " float lum = dot(gl_FragColor.rgb, vec3(0.299, 0.587, 0.114));", + " ", + " // Screen Space Coordinates", + " vec2 screenPos = gl_FragCoord.xy;", + " ", + " // Rotated Grid Calculation (45 degrees for classic look)", + " float s = sin(0.785398); // 45 deg", + " float c = cos(0.785398);", + " vec2 rotPos = vec2(", + " c * screenPos.x - s * screenPos.y,", + " s * screenPos.x + c * screenPos.y", + " ) * htScale;", + " ", + " // Dot Pattern (Sine Wave Grid)", + " float pattern = (sin(rotPos.x) * sin(rotPos.y)) * 4.0;", + " ", + " // Threshold based on luminance", + " // Darker areas (low lum) get more dots", + " float threshold = (lum * 10.0) - 5.0;", + " ", + " // Apply Halftone", + " if (pattern > threshold) {", + " // Interpolate towards halftone color based on intensity", + " // Use 'mix' for smoother transition or 'if' for hard dots", + " // We use a soft mix for professional look", + " float mixFactor = clamp((pattern - threshold) * 2.0, 0.0, 1.0);", + " gl_FragColor.rgb = mix(gl_FragColor.rgb, htColor, mixFactor * htIntensity);", + " }", + " `", + " );", + " };", + " material.needsUpdate = true;", + " }", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + }, + { + "description": "Dot Color (Usually Black or Dark Blue)", + "name": "Color", + "type": "color" + }, + { + "description": "Dot Scale (Lower is bigger dots! Try 0.5 to 1.5)", + "name": "Scale", + "type": "expression" + }, + { + "description": "Intensity (Opacity of dots, 0.0 to 1.0)", + "name": "Intensity", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Apply a professional shadow tint to the dark areas of an object. Allows giving shadows a custom color (like purple or blue) for an anime look.", + "fullName": "Apply Shadow Tinting", + "functionType": "Action", + "group": "Effects", + "name": "ApplyShadowTint", + "sentence": "Apply shadow tint to _PARAM1_ (Color: _PARAM2_, Intensity: _PARAM3_, Threshold: _PARAM4_, Softness: _PARAM5_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const tintColor = gdjs.rgbOrHexStringToNumber(eventsFunctionContext.getArgument(\"Color\"));", + "const intensity = eventsFunctionContext.getArgument(\"Intensity\");", + "const threshold = eventsFunctionContext.getArgument(\"Threshold\");", + "const softness = eventsFunctionContext.getArgument(\"Softness\");", + "// Default light direction if not provided (Top-Right-Front)", + "const lx = 0.5, ly = 0.8, lz = 0.5;", + "", + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " if (child.userData.isToonOutline || child.userData.isToonFill) return;", + " ", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " materials.forEach(material => {", + " if (material.onBeforeCompile !== undefined) {", + " if (material.userData.tintUniforms) {", + " material.userData.tintUniforms.shadowColor.value.setHex(tintColor);", + " material.userData.tintUniforms.tintIntensity.value = intensity;", + " material.userData.tintUniforms.tintThreshold.value = threshold;", + " material.userData.tintUniforms.tintSoftness.value = softness;", + " material.needsUpdate = true;", + " } else {", + " material.userData.tintUniforms = {", + " shadowColor: { value: new THREE.Color(tintColor) },", + " tintIntensity: { value: intensity },", + " tintThreshold: { value: threshold },", + " tintSoftness: { value: softness },", + " shadowLightDir: { value: new THREE.Vector3(lx, ly, lz).normalize() }", + " };", + "", + " const oldOnBeforeCompile = material.onBeforeCompile;", + " material.onBeforeCompile = (shader) => {", + " if (oldOnBeforeCompile) oldOnBeforeCompile(shader);", + " Object.assign(shader.uniforms, material.userData.tintUniforms);", + "", + " // 1. Vertex Shader (Ensure normals for basic materials)", + " if (!shader.vertexShader.includes('varying vec3 vToonNormal')) {", + " shader.vertexShader = shader.vertexShader.replace('#include ', '#include \\nvarying vec3 vToonNormal;\\nvarying vec3 vToonViewPos;\\n');", + " shader.vertexShader = shader.vertexShader.replace('#include ', '#include \\nvToonNormal = normalize(normalMatrix * normal);\\nvToonViewPos = -mvPosition.xyz;');", + " }", + "", + " // 2. Fragment Shader Injection", + " if (!shader.fragmentShader.includes('uniform vec3 shadowColor')) {", + " shader.fragmentShader = shader.fragmentShader.replace('#include ', '#include \\nuniform vec3 shadowColor;\\nuniform float tintIntensity;\\nuniform float tintThreshold;\\nuniform float tintSoftness;\\nuniform vec3 shadowLightDir;\\nvarying vec3 vToonNormal;\\n');", + " }", + "", + " shader.fragmentShader = shader.fragmentShader.replace(", + " '#include ',", + " `", + " #include ", + " ", + " // Calculate N.L for shadowing", + " vec3 n_tint = normalize(vToonNormal);", + " vec3 l_tint = normalize(shadowLightDir);", + " float nl_tint = dot(n_tint, l_tint);", + " ", + " // Map -1..1 to 0..1", + " float lightFactor = nl_tint * 0.5 + 0.5;", + " ", + " // Step function for Toon look", + " float edge1 = tintThreshold - (tintSoftness * 0.5);", + " float edge2 = tintThreshold + (tintSoftness * 0.5);", + " float shadowMask = smoothstep(edge1, edge2, lightFactor);", + " ", + " // Professional Shadow Tinting (Multiply base by shadow color in dark areas)", + " // We mix between original color and original*shadowColor", + " vec3 darkColor = gl_FragColor.rgb * shadowColor;", + " vec3 tinted = mix(darkColor, gl_FragColor.rgb, shadowMask);", + " ", + " // Blend final result based on intensity", + " gl_FragColor.rgb = mix(gl_FragColor.rgb, tinted, tintIntensity);", + " `", + " );", + " };", + " material.needsUpdate = true;", + " }", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + }, + { + "description": "Shadow Tint Color (e.g. Purple, Navy, Dark Red)", + "name": "Color", + "type": "color" + }, + { + "description": "Intensity (0.0 to 1.0)", + "name": "Intensity", + "type": "expression" + }, + { + "description": "Threshold (0.3 to 0.7, determines where shadow starts)", + "name": "Threshold", + "type": "expression" + }, + { + "description": "Softness (0.0 for hard edge, 0.2 for soft anime blend)", + "name": "Softness", + "type": "expression" + } + ], + "objectGroups": [] + }, + { + "description": "Remove shadow tinting from an object.", + "fullName": "Remove Shadow Tinting", + "functionType": "Action", + "group": "Effects", + "name": "RemoveShadowTint", + "sentence": "Remove shadow tint from _PARAM1_", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "objects.forEach(object => {", + " const rootObject = object.get3DRendererObject();", + " if (!rootObject) return;", + "", + " rootObject.traverse((child) => {", + " if (child.isMesh && child.material) {", + " const materials = Array.isArray(child.material) ? child.material : [child.material];", + " materials.forEach(material => {", + " if (material.userData.tintUniforms) {", + " material.userData.tintUniforms.tintIntensity.value = 0;", + " material.needsUpdate = true;", + " }", + " });", + " }", + " });", + "});" + ], + "parameterObjects": "Object", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Object", + "name": "Object", + "type": "objectList" + } + ], + "objectGroups": [] + }, + { + "description": "Apply shadow tinting to all objects on a layer.", + "fullName": "Apply Shadow Tint to Layer", + "functionType": "Action", + "group": "Layer Effects", + "name": "ApplyShadowTintToLayer", + "sentence": "Apply shadow tint to layer _PARAM1_ (Color: _PARAM2_, Intensity: _PARAM3_, Threshold: _PARAM4_, Softness: _PARAM5_)", + "events": [ + { + "type": "BuiltinCommonInstructions::JsCode", + "inlineCode": [ + "const layerName = eventsFunctionContext.getArgument(\"Layer\");", + "const color = eventsFunctionContext.getArgument(\"Color\");", + "const intensity = eventsFunctionContext.getArgument(\"Intensity\");", + "const threshold = eventsFunctionContext.getArgument(\"Threshold\");", + "const softness = eventsFunctionContext.getArgument(\"Softness\");", + "", + "const instances = [];", + "const objectNames = runtimeScene.getObjects().map(o => o.getName());", + "objectNames.forEach(name => {", + " const list = runtimeScene.getInstancesOfObject(name);", + " for (let i = 0; i < list.length; i++) {", + " if (list[i].getLayer() === layerName) {", + " instances.push(list[i]);", + " }", + " }", + "});", + "", + "if (instances.length > 0) {", + " eventsFunctionContext.getEventsFunctionContext()._extToonShaderApplyShadowTint(instances, color, intensity, threshold, softness);", + "}" + ], + "parameterObjects": "", + "useStrict": true, + "eventsSheetExpanded": true + } + ], + "parameters": [ + { + "description": "Layer", + "name": "Layer", + "type": "layer" + }, + { + "description": "Shadow Color", + "name": "Color", + "type": "color" + }, + { + "description": "Intensity", + "name": "Intensity", + "type": "expression" + }, + { + "description": "Threshold", + "name": "Threshold", + "type": "expression" + }, + { + "description": "Softness", + "name": "Softness", + "type": "expression" + } + ], + "objectGroups": [] + } + ], + "eventsBasedBehaviors": [], + "eventsBasedObjects": [] +} \ No newline at end of file