Drambo Art

1356717

Comments

  • I gotta comment on this.

    First of all fluid dynamics are f’ing hard. Or I just can’t get it.

    I like the XY pad analogy though. I mean I don’t want to spend 100€ or whatever it’s priced on a pencil. But my poor man’s alternative has always been the pad.

    that said I wish guys with a pencil to have that freedom. Count me out but I don’t want to spoil your fun

  • reading my comment I don’t think I expressed myself the best way but hopefully you’ll know I mean well

  • Well, I can certainly suggest plenty of things but I don’t have the technical/coding knowledge to know whether my suggestions are at all possible within the Drambo framework!

  • Would be really interested to try this xy pad input method of yours, please do share if you can find the project. Cheers.

  • No, I know what you mean. I don't mean suggesting actual Apple Pencil support either, only a "drawing pad" input module; similar to the XY pad, but with more features for drawing built in. Users can use their finger, a stylus, or whatever they're most comfortable drawing with.

    I think it was @tyslothrop1 that originally suggested such a thing back in the beta period.

  • I have it saved. Let me pull it up, and dust it off. Make sure it works the way I remember.

  • edited July 30

    @Robin Here it is. 🙂

    Note there's various settings to control ink flow, size, bleed amount (for the water color effect), etc.

    The graphic envelope controls the "start painting" and stop/release when you lift your finger/stylus. This curve can be customized to your liking of course, as with everything else.

    In this save, I have a randomizer controlling the paint/ink color to have a new color for each new stroke. Remove this if you want to work with select colors.

    Btw, let me know if you can think of any ways of improving this.

    Thanks and have fun!

  • @Intrepolicious I noticed there is a water color example at metalshaders.com. Did you use it for this one? If not, then perhaps there are some improvements to be found there.

    I don’t have a pencil. Does it feel good to use on an xy pad, or are there issues like not high enough sampling frequency when doing fast strokes?

  • Brilliant, thanks @Intrepolicious. Got to go to work unfortunately but will be checking it out once I’m back! Cheers.

  • No, I didnt see that. Will have to take a look.

    its 'okay'. I only recently picked up the Apple Pencil Pro. Never had one, nor have a ever really used a stylus. I've always gravitated to using my finger. I just have to 'Remember' to use it.

    As far as sampling frequency, this patch was geared as a 'water color' painting kit, with broad, bleeding strokes. Trying to quickly convert it to a writing tablet for the above post was tricky. It needs some fine adjustments to the ink flow, pressure, etc for finer lines, other than just making the brush size small.

  • Have a good day at work. Let me know what you think later.

  • I was just thinking that this is just like using a Wacom tablet..

    What would really be cool, would be being able to draw/paint right on the projector screen.

  • Yes, I 'd love to be able to draw into a running visualisation. @Intrepolicious that screenshot looks like your patch is pretty usable. Gotta check that out. I used to do some doodling with a wacom ripoff.

    If there at some point will be something like a canvas module, it would be cool, if you could modulate the canvas itself, like zoom and angle. Might be cool to paint on a spinning surface (:

  • Btw @Robin I was just looking at that 'Mental Canvas' app, which I'd never seen before. (I do have Feather)

    It's an amazing looking app, but the subscription ughh, they don't have a one time purchase option?

  • Yes, touch support on projector was asked by some user at launch, too. @giku has probably considered it. I wonder if it could be a “projector rack” using feedback send/receive, where the touch is sent back to earlier modules and used for live drawing by shaders.

    Another use for that setup could be a rack with camera + projector. If the touch on projector could feedback and auto-connect with relevant camera knobs, you could have pencil pressure and tilt controlling camera yaw, roll, zoom.

  • Hey @Intrepolicious, just been playing around with your watercolour shader. It’s brilliant, works really well. Yes, no different from using Wacom type graphics tablet.

    My immediate thought was whether it’s possible to record the interaction with the xy pad and, of course, it is via recorded automation. It works for playback…but I can’t get it to export successfully? And the trigger button to clear doesn’t seem to work, the clear dial has to be manually turned to clear the canvas.

    The painted canvas seems hard linked to the current projector - closing and reopening the projector rescales the image smaller - is this unavoidable? I suspect the reason xy pad automation isn’t exporting successfully is probably linked to this behaviour?

    So many possibilities though if things can ironed out. Personally, I’d prefer a non watercolour type regular solid line which could be thinner but no doubt this is easily achieved? And I’d like black and white options in the palette too. Do you mind me asking AI to adapt the current code - no worries at all if you’d rather I not.

    Thanks so much for sharing!

    Regarding Mental Canvas, I’ve just checked and I’ve got the Creator Plan which was a one time purchase several years ago. Looking at their website, it seems this option is no longer available which is crap. Previous purchasers of the Creator Plan will retain lifetime access to the features they purchased but for new features and new customers it seems to be subscription only. It has some nice features for sure but you can pretty much achieve the same, and more. in Feather.

    I also recommend taking a look at Infinite Zoom Drawing Canvas if you’re not aware of it (https://apps.apple.com/gb/app/infinite-zoom-drawing-canvas/id6757370793). Don’t know how much it is now but it was cheap when I bought it on release six months ago and it’s had an astonishing amount of features added since release. Not the most polished app but it’s a fun drawing/animation playground and also allows the Feather/Mental Canvas type of user defined journey through your canvas (with the addition of having parts of your drawing also animated within themselves). Would easily allow for exporting videos to use within Drambo Visual…and set the background of your drawing/exported video to a solid colour and use Drambo’s Chroma Key to remove it for even more fun!

    Cheers.

  • I’ve also suggested in the past that the Pencil and its pressure sensitivity and tilt sensitivity could make for a really interesting MIDI controller module.

  • Of course, I'm happy to share it. And of course again, go for it! You have the code, do with it as you please. Just please share back and show us what you come up with.

    For the Clear via button, I swear it worked at one point. Maybe stick another graphic envelope in between the button, and the knob to get it working.

    And the export thing, I don't think I tried that with this patch. One thing that comes to mind, if it isn't working, I'd try inserting a shader processor with the standard Transform scale preset after everything, and right before the projector. The feedback modules were necessary (with a shader) to have the paint strokes persist.

    Come to think of it, I think this was put together before the Scene module came about. Maybe we could do a "scene version"? Scenes are stateful and might be more export friendly.

  • I played around with it, too and like it a lot. Thanks @Intrepolicious!

    No idea, why the clear button isn't working, but if you replace the trigger button with a buttons module and set it to momentary, it works.

    About finer lines, if you modulate the size knob with a slider, but feed it to a graphic shaper first, set to a unipolar exponential rising curve, you have finer resolution for finer lines.

  • Awesome @tyslothrop1 I'm so glad you guys like it!

    Oh, btw @Robin , I was looking, and I do in fact also have that Infinite Zoom app as well as Feather. I love your ideas for the exported video and using them in Drambo with the chroma key effect

  • edited July 31

    Hi @Intrepolicious. So, I’ve had an initial play around adapting your shader code using Gemini.

    I’ve asked it to make it more of a solid line like a technical pen and added black and white to the palette. I also asked it to add Fade and Spread parameters so the technical pen type line can actually become quite watercolour like again!

    Fade can be off where the lines remain permanently and then the parameter controls the speed at which the drawn line fades away. Spread can be off and then controls the amount the line spreads and blurs.

    https://youtube.com/shorts/oAqPJMu5Or0?si=OoN4enefFXdbEZSu

    I think currently the shader setup of yours will be of most use for painting/automating effects rather than drawing images per se. For showing images being drawn, I think it would still be more effective to use, for example, Procreate, set the background to a solid colour, and export the video of the time lapse to use in Visual using the chroma key as I mentioned before to remove the background (if desired).

    Not sure the best way to share the adapted shader (couldn’t find the exported preset?) but here is the code. Cheers.


    float3 paletteFun(float t) {

      if (t < 0.15) {

        return float3(0.1, 0.1, 0.1);

      } else if (t < 0.3) {

        return float3(0.95, 0.95, 0.95);

      } else {

        float shifted = (t - 0.3) / 0.7;

        return float3(

          0.5 + 0.5 * cos(6.28318 * (shifted + 0.0)),

          0.5 + 0.5 * cos(6.28318 * (shifted + 0.33)),

          0.5 + 0.5 * cos(6.28318 * (shifted + 0.67))

        );

      }

    }


    float4 render(device Data &data, uint2 tid, float2 uv [[input]], texture2d<float> InTexture [[input]], float Brush_X [[param,bipolar]], float Brush_Y [[param,bipolar]], float Palette [[param]], float Size [[param]], float Pressure [[param]], float Flow [[param]], float Fade [[param]], float Spread [[param]], float Clear [[param]]) {

      texture2d<float> fb = TEXTURE_BUFFER(RGB32);

      float4 canvas = fb.read(tid);

      float2 brushPos = float2(Brush_X, -Brush_Y);

       

      // Read the secret brush state from pixel (0,0)

      float4 statePx = fb.read(uint2(0u, 0u));

      float2 prevBrushPos = statePx.xy;

      float3 bgColor = float3(0.98, 0.97, 0.94);

       

      bool doClear = length(canvas.rgb) < 0.001 || Clear > 0.5;

      if (doClear) {

        prevBrushPos = brushPos;

      }

       

      float3 canvasRGB = canvas.rgb;

       

      // --- Extreme Expand and Blur Logic (9-Tap Box Blur) ---

      if (Spread > 0.001 && !doClear) {

        uint w = uint(data.width);

        uint h = uint(data.height);

         

        // Calculate orthagonal and diagonal coordinates safely

        uint2 tN = uint2(tid.x, min(tid.y + 1u, h - 1u));

        uint2 tS = uint2(tid.x, tid.y > 0u ? tid.y - 1u : 0u);

        uint2 tE = uint2(min(tid.x + 1u, w - 1u), tid.y);

        uint2 tW = uint2(tid.x > 0u ? tid.x - 1u : 0u, tid.y);

         

        uint2 tNE = uint2(tE.x, tN.y);

        uint2 tNW = uint2(tW.x, tN.y);

        uint2 tSE = uint2(tE.x, tS.y);

        uint2 tSW = uint2(tW.x, tS.y);

         

        // Safely sample all 8 neighbors (bypassing the secret coordinate at 0,0)

        float3 cN = (tN.x == 0u && tN.y == 0u) ? canvasRGB : fb.read(tN).rgb;

        float3 cS = (tS.x == 0u && tS.y == 0u) ? canvasRGB : fb.read(tS).rgb;

        float3 cE = (tE.x == 0u && tE.y == 0u) ? canvasRGB : fb.read(tE).rgb;

        float3 cW = (tW.x == 0u && tW.y == 0u) ? canvasRGB : fb.read(tW).rgb;

        float3 cNE = (tNE.x == 0u && tNE.y == 0u) ? canvasRGB : fb.read(tNE).rgb;

        float3 cNW = (tNW.x == 0u && tNW.y == 0u) ? canvasRGB : fb.read(tNW).rgb;

        float3 cSE = (tSE.x == 0u && tSE.y == 0u) ? canvasRGB : fb.read(tSE).rgb;

        float3 cSW = (tSW.x == 0u && tSW.y == 0u) ? canvasRGB : fb.read(tSW).rgb;

         

        // Average all 9 pixels (center + 8 neighbors)

        float3 blurCol = (canvasRGB + cN + cS + cE + cW + cNE + cNW + cSE + cSW) * 0.111111;

         

        // Squaring the parameter gives fine control at low ranges, but reaches a full 100% replacement at max

        float extremeSpread = Spread * Spread; 

        canvasRGB = mix(canvasRGB, blurCol, extremeSpread);

      }

       

      // --- Fade Logic ---

      if (Fade > 0.001 && !doClear) {

        canvasRGB = mix(canvasRGB, bgColor, Fade * 0.05);

      }

       

      // Continuous line SDF geometry

      float2 pa = uv - prevBrushPos;

      float2 ba = brushPos - prevBrushPos;

      float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);

      float dist = length(pa - ba * h);

       

      // Draw the sharp pen stroke

      float brushRadius = Size * 0.1;

      float aa = 2.0 / max(float(data.width), float(data.height));

      float inkMask = 1.0 - smoothstep(max(0.0, brushRadius - aa), brushRadius + aa, dist);

      inkMask *= saturate(Pressure * Flow);

       

      float3 paintColor = paletteFun(Palette);

      float3 blendedRGB = mix(canvasRGB, paintColor, inkMask);

       

      if (doClear) {

        blendedRGB = bgColor;

      }

       

      float4 outColor = float4(saturate(blendedRGB), 1.0);

      float4 fbOut = outColor;

       

      // Protect the state pixel at (0,0)

      if (tid.x == 0u && tid.y == 0u) {

        fbOut = float4(brushPos.x, brushPos.y, 0.0, 1.0);

        outColor = float4(bgColor, 1.0);

      }

       

      fb.write(fbOut, tid);

      return outColor;

    }

  • After failing to do so on that last post, how do you embed a video in a post rather than just having the link?

  • Cool @Robin! I’m at work rn

    will have to check out you adaptation later.

    to answer your last question re YT vids, paste the link in the circled:


  • edited July 31

    It’s different over on LPF where you can just post the link

  • @Robin I finally got your code loaded up. I like it!

    While we do indeed have a working proof of concept here, I think you're probably right, regarding doing the artwork in a third party app, then bringing it into Drambo via image/video. Especially for any kind of serious artwork.

    This is definitely fun to play with though, as I'm sure you agree. 😊


    btw, the best/easiest way to share a shader, or rack of any type, is to just share the Drambo project file.

  • Oh, it’s absolutely fun and there will be genuine uses for it!

    I’ve played around via Gemini again and converted it into a processor. This will definitely have some uses. Has several parameters to give more control than this rough example might suggest.


    Code to replace the code in your original shader with.


    float4 render(device Data &data, uint2 tid, float2 uv [[input]], texture2d<float> InTexture [[input]], float Brush_X [[param,bipolar]], float Brush_Y [[param,bipolar]], float Size [[param]], float Pressure [[param]], float Flow [[param]], float Opacity [[param]], float Fade [[param]], float Spread [[param]], float Velocity [[param]], float Clear [[param]]) {

      sampler s(filter::linear, address::repeat, mip_filter::linear);

       

      texture2d<float> fb = TEXTURE_BUFFER(RGB32);

      float4 canvas = fb.read(tid);

      float2 brushPos = float2(Brush_X, -Brush_Y);

       

      // Read previous position stored in pixel (0,0)

      float4 statePx = fb.read(uint2(0u, 0u));

      float2 prevBrushPos = (statePx.z > -0.5) ? statePx.xy : brushPos;

       

      bool doClear = Clear > 0.5;

      if (doClear) {

        prevBrushPos = brushPos;

      }

       

      // Screen aspect ratio for geometric compensation

      float aspect = float(data.width) / float(data.height);

       

      // Unpack canvas: R/G stores displacement vectors (dx, dy), Alpha stores motion trail mask

      float2 canvasFlow = canvas.rg;

      float canvasAlpha = canvas.a;

       

      // --- True Aspect-Corrected Circular Spread / Radial Dilation Logic ---

      if (Spread > 0.001 && !doClear) {

        uint w = uint(data.width);

        uint h = uint(data.height);

         

        float maxAlpha = canvasAlpha;

        float2 dilatedFlow = canvasFlow;

         

        float totalWeight = 1.0;

        float2 weightedFlow = canvasFlow * totalWeight;

        float weightedAlpha = canvasAlpha * totalWeight;

         

        float radius = 1.0 + Spread * 2.0;

        int intRadius = int(ceil(radius));

         

        for (int y = -intRadius; y <= intRadius; y++) {

          for (int x = -intRadius; x <= intRadius; x++) {

            if (x == 0 && y == 0) continue;

             

            // Scale horizontal offset by aspect ratio to maintain a true circle

            float2 offset = float2(float(x) * aspect, float(y));

            float dist = length(offset);

             

            if (dist > radius) continue;

             

            uint2 sampleCoord = uint2(

              clamp(int(tid.x) + x, 0, int(w) - 1),

              clamp(int(tid.y) + y, 0, int(h) - 1)

            );

             

            float4 samplePx = (sampleCoord.x == 0u && sampleCoord.y == 0u) ? float4(canvasFlow, 0.0, canvasAlpha) : fb.read(sampleCoord);

             

            if (samplePx.a > maxAlpha) {

              maxAlpha = samplePx.a;

              dilatedFlow = samplePx.rg;

            }

             

            float weight = saturate(1.0 - (dist / (radius + 0.01)));

            weightedFlow += samplePx.rg * weight;

            weightedAlpha += samplePx.a * weight;

            totalWeight += weight;

          }

        }

         

        if (totalWeight > 0.0) {

          weightedFlow /= totalWeight;

          weightedAlpha /= totalWeight;

        }

         

        float extremeSpread = Spread * Spread;

        float2 targetFlow = mix(dilatedFlow, weightedFlow, 0.4);

        float targetAlpha = max(maxAlpha, weightedAlpha);

         

        canvasFlow = mix(canvasFlow, targetFlow, extremeSpread);

        canvasAlpha = mix(canvasAlpha, targetAlpha, extremeSpread);

      }

       

      // --- Fade Logic ---

      if (Fade > 0.001 && !doClear) {

        canvasFlow *= (1.0 - Fade * 0.05);

        canvasAlpha *= (1.0 - Fade * 0.05);

      }

       

      // --- Configurable Velocity-Sensitive Calligraphic Sizing ---

      float2 strokeVector = (brushPos - prevBrushPos);

      float strokeSpeed = length(strokeVector);

       

      // Modulate size adjustment scale based on user Velocity parameter

      float speedFactor = saturate(strokeSpeed * 15.0);

      float dynamicSizeScale = mix(1.0, mix(1.3, 0.5, speedFactor), Velocity);

       

      // Aspect-corrected SDF distance calculation for current stroke line

      float2 aspectUV = uv;

      aspectUV.x *= aspect;

      float2 aspectPrev = prevBrushPos;

      aspectPrev.x *= aspect;

      float2 aspectCurr = brushPos;

      aspectCurr.x *= aspect;

       

      float2 pa = aspectUV - aspectPrev;

      float2 ba = aspectCurr - aspectPrev;

      float h = clamp(dot(pa, ba) / max(dot(ba, ba), 1e-6), 0.0, 1.0);

      float dist = length(pa - ba * h);

       

      float brushRadius = Size * 0.1 * dynamicSizeScale;

      float aa = 2.0 / max(float(data.width), float(data.height));

      float inkMask = 1.0 - smoothstep(max(0.0, brushRadius - aa), brushRadius + aa, dist);

       

      inkMask *= saturate(Pressure * Flow * Opacity);

       

      // Accumulate displacement flow vectors along the motion trajectory path

      float2 combinedFlow = canvasFlow + (strokeVector * inkMask * 5.0);

      float combinedAlpha = saturate(canvasAlpha + inkMask);

       

      if (doClear) {

        combinedFlow = float2(0.0);

        combinedAlpha = 0.0;

      }

       

      float4 fbOut = float4(saturate(combinedFlow), 0.0, saturate(combinedAlpha));

       

      // Protect state pixel at (0,0)

      if (tid.x == 0u && tid.y == 0u) {

        fbOut = float4(brushPos.x, brushPos.y, 1.0, 1.0);

      }

      fb.write(fbOut, tid);

       

      // --- Final Displacement Sample ---

      float2 displacementUV = textureUV(data, uv) - (combinedFlow * 0.1);

      float4 displacedColor = InTexture.sample(s, displacementUV);

       

      if (tid.x == 0u && tid.y == 0u) {

        displacedColor = InTexture.sample(s, textureUV(data, uv));

      }

       

      return float4(saturate(displacedColor.rgb), 1.0);

    }

  • edited August 1

    Don’t want this to sound as a criticism but it would be really better if you just posted the .project file

    save in icloud use the clip here and go to the file there in icloud. At least that’s how I do it

  • Yes, sorry, you’re right…no one needs epic posts of code!

  • You can also copy the code to a text file and upload the txt here as the forum allows it. But anyway thanks for sharing

  • edited August 3

    Inspired by @Intrepolicious meadow flowers, and deriving from @giku ’s preset 2d piano roll, I’ve been working on a flower field piano roll. Pretty minimal for now, gotta turn it 3D 😅 it’s a work in progress but I welcome suggestions or criticism

    btw, this is of course more fun with a midi keyboard.

    Also if you want to use the code and build on it, be my guest. I don’t have much time to spend on drambo, lately. Barely tested the new grain and looper modules, much to my regret

Sign In or Register to comment.