GIMP Layers to SL Animated Texture

From Second Life Wiki
Jump to navigation Jump to search

This is a GIMP script (written in Script-Fu, the Scheme-based scripting language that GIMP uses) that takes the layers of an image representing an animation, and converts them into a texture suitable for use with llSetTextureAnim.


Copy and paste the script below into a simple text editor of your choice (Notepad is probably the best choice for Windows systems). Save it into your scripts folder with the extension .scm - the suggested name is "layers-to-sl-animation.scm". The scripts folder can be found by going to Preferences > Folders > Scripts. After it's in the Scripts folder, do a refresh by selecting Filters > Script-Fu > Refresh Scripts.


First, create or load an animation with one layer per frame (the first frame is the bottom layer). An existing GIF animation will work for this purpose, or you can create your own, or import one if you have enough memory. Version 1.0 of this script required to use the Unoptimize filter first in some cases; as of version 2.0, that functionality is already built in.

Next, ensure that it has the correct size and number of frames desired. It's preferable to have a plan at this point of what the final resolution will be, and how many horizontal and vertical frames the final image will have. If you are not going to use all the frames in your final image (for example, you have 5 frames and decide to do a 3x2 image), you still need to fill the holes. Create empty layers or duplicate existing ones until completing the final frame count. If you need to rescale the image at this point and the image is indexed, it's better if you convert it to RGB mode first.

Now invoke the script. It is invoked using Script-fu > SecondLife > Frames to texture...

A window will appear, asking the number of horizontal and vertical frames of the final image. As noted above, that number must match the number of frames in your layers, or the script will show an error instead. It also asks whether you want to generate an animation script in a message window, and the frame rate to use for that script. Click OK and you will get a new image window consisting of the original frames arranged according to the specified values, suitable for use in SL. Now you can save the file and upload it to SL. If you had the script generation option checked, you can select the code from the window that appears, copy it, and paste it as a LSL script replacing the contents.

The script

; Layers to SL Texture Animation, version 2.0.2
; Written by Pedro Oval, 2010-12-04.
; Donated to the public domain.
; Thanks to Digital Dharma for the suggestions.

(define (script-fu-layers-to-sl-anim curimg curdrawable xframes yframes showscript face fps)
 (let ()

  ; Local function: check if a string contains a substring
  (define (contains-ci str substr)
    (let* (
           (i 0)
           (lensub (string-length substr))
           (imax (- (string-length str) lensub))
      (if (negative? imax)
          (define found #f)
          (while (and (not found) (<= i imax))
            ; We make a case-insensitive comparison.
            (if (string-ci=? (substring str i (+ i lensub)) substr)
              (set! found #t)
              (set! i (+ i 1))))

  ; Read the layer IDs and the total number of layers from the original image.
  (define layerset (gimp-image-get-layers curimg))
  (define numframes (car layerset))
  (set! layerset (cadr layerset))

  ; The frame size X and Y is the size of the original image.
  (define framesizex (car (gimp-image-width curimg)))
  (define framesizey (car (gimp-image-height curimg)))

  ; Get the image type; indexed images need special treatment.
  (define imgtype (car (gimp-image-base-type curimg)))

  ; Check if the given number of horizontal x vertical frames matches
  ; the number of layers in the original image. If yes, proceed; if not,
  ; show an error message.
  (if (= numframes (* xframes yframes))
      ; New image
      (define img (car (gimp-image-new (* framesizex xframes) (* framesizey yframes) imgtype)))
      ; We create a new image, so we disable undo for it; to "undo"
      ; the script, just close the new image.
      (gimp-image-undo-disable img)

      (if (= imgtype INDEXED)
          ; Indexed images need the palette to be copied to the new image.
          (define palette (gimp-image-get-colormap curimg))
          (gimp-image-set-colormap img (car palette) (cadr palette))))

      ; Initialize previous layer info to 0, as there's no previous layer yet.
      ; On the next iteration it will hold a list with:
      ;  layer ID, offset X, offset Y.
      (define prevlayerinfo 0)

      ; Loop betwen 0 and numframes - 1.
      (define frame 0)
      (while (< frame numframes)
        ; For each layer/frame:

          ; Show progress as the current frame relative to the number of frames.
          (gimp-progress-update (/ frame numframes))

          ; Read the individual layer ID, starting from the bottom.
          (define oldlayer (vector-ref layerset (- numframes frame 1)))

          ; Create new layer as a copy of this one and add it to the image.
          (define newlayer (car (gimp-layer-new-from-drawable oldlayer img)))
          (gimp-image-add-layer img newlayer -1)

          ; Add alpha if the layer doesn't have it.
          (if (= (car (gimp-drawable-has-alpha newlayer)) FALSE)
            (gimp-layer-add-alpha newlayer))

          ; Calculate the offsets relative to the destination rectangle,
          ; and the crop size.
          (define offsets (gimp-drawable-offsets oldlayer))
          (define newsizex (car (gimp-drawable-width oldlayer)))
          (define newsizey (car (gimp-drawable-height oldlayer)))
          (define offsetx (car offsets))
          (define offsety (cadr offsets))
          (if (< offsetx 0)
              (set! newsizex (+ newsizex offsetx))
              (set! offsetx 0)))
          (if (< offsety 0)
              (set! newsizey (+ newsizey offsety))
              (set! offsety 0)))
          (if (> (+ offsetx newsizex) framesizex)
            (set! newsizex (- framesizex offsetx)))
          (if (> (+ offsety newsizey) framesizey)
            (set! newsizey (- framesizey offsety)))

          ; Calculate frame rectangle's top left position.
          (define framey (truncate (/ frame xframes)))
          (define framex (- frame (* framey xframes)))
          (set! framex (* framex framesizex))
          (set! framey (* framey framesizey))

          ; Crop the layer if necessary so that it fits its
          ; rectangle, and move it to its final position.
          (gimp-layer-resize newlayer
                             (- (car offsets) offsetx)
                             (- (cadr offsets) offsety))
          (gimp-layer-set-offsets newlayer (+ framex offsetx) (+ framey offsety))

          ; Force it visible.
          (gimp-drawable-set-visible newlayer TRUE)

          ; Check for (combine) mode. Note: It just works
          ; in layers other than 0. Otherwise no action is
          ; done. It makes no sense for the bottom layer to
          ; use combine mode anyway.
          (if (and (> frame 0) (contains-ci (car (gimp-drawable-get-name oldlayer)) "(combine)"))
              ; Duplicate the previous layer to serve as a base
              ; for the differencing frame being added.
              (define layercopy (car (gimp-layer-new-from-drawable (car prevlayerinfo) img)))

              ; Add the new layer below the last layer.
              (gimp-image-add-layer img layercopy 1)

              ; Set the layer position to match that of the new layer.
              (gimp-layer-set-offsets layercopy (+ framex (cadr prevlayerinfo))
                                                (+ framey (caddr prevlayerinfo)))

              ; Ensure the previous layer's copy is visible.
              (gimp-drawable-set-visible layercopy TRUE)

              ; Merge the differencing frame into the previous layer's copy.
              (set! newlayer (car (gimp-image-merge-down img newlayer EXPAND-AS-NECESSARY)))

              ; Correct the offsets. We use 'min' to match what
              ; EXPAND-AS-NECESSARY has supposedly done. Ideally
              ; we should read the new offsets and subtract
              ; framex/framey from them, but this is faster and simpler,
              (set! offsetx (min offsetx (cadr prevlayerinfo)))
              (set! offsety (min offsety (caddr prevlayerinfo)))))

          ; Previous layer = this.
          (set! prevlayerinfo (list newlayer offsetx offsety))

          ; All done. Next loop iteration.
          (set! frame (+ frame 1))))

      ; Progress 100%
      (gimp-progress-update 1.0)

      ; Merge visible layers (all are visible now).
      (define mergedlayer (car (gimp-image-merge-visible-layers img CLIP-TO-IMAGE)))

      ; Make the new layer the size of the image.
      (gimp-layer-resize-to-image-size mergedlayer)

      ; Convert the image to RGB if it is not already.
      (if (not (= imgtype RGB))
        (gimp-image-convert-rgb img))

      ; Detect alpha by converting it to a selection.
      (gimp-selection-layer-alpha mergedlayer)

      ; If there is no useful alpha, the selection should cover the whole image.
      ; Invert it, and if it becomes empty, then it means that alpha was not
      ; in use: the whole image was covered with 100% opacity.
      (gimp-selection-invert img)
      (define hasalpha (car (gimp-selection-bounds img)))
      (gimp-selection-none img)

      ; If it didn't have any alpha, remove the alpha mask from the layer.
      (if (= hasalpha FALSE)
        (gimp-layer-flatten mergedlayer))

      ; All done - enable undo again and add a display window for the new image.
      (gimp-image-undo-enable img)
      (gimp-display-new img)

      ; Prepare a version of fps ending in .0 if it doesn't.
      (define fpsstr (number->string fps))
      (if (not (contains-ci fpsstr "."))
        (set! fpsstr (string-append fpsstr ".0")))

      ; Show the script if requested
      (if (= showscript TRUE)
        (gimp-message (string-append "// Copy-paste the following fragment as a script:\n\n"
                                     "default\n{\n    state_entry()\n    {\n"
                                     "        llSetTextureAnim(ANIM_ON | LOOP, "
                                     ", "
                                     (number->string xframes)
                                     ", "
                                     (number->string yframes)
                                     ", 0.0, "
                                     (number->string (* xframes yframes))
                                     ".0, "
                                     fpsstr ");\n    }\n}\n"))))

    ; If the frame counts didn't match...
    (gimp-message "Error: Number of layers doesn't match horizontal x vertical frames"))))

; Register the function.
(script-fu-register "script-fu-layers-to-sl-anim"
                    _"<Image>/Script-Fu/SecondLife/Frames to texture..."
                    _"Convert a set of layers (frames) to a texture suitable for llSetTextureAnim."
                    "Pedro Oval"
                    _"Public Domain"
                    "RGB*, GRAY*, INDEXED*"
                    SF-IMAGE        "Image"        0
                    SF-DRAWABLE     "Drawable"     0
                    SF-ADJUSTMENT   _"# of horizontal frames" '(2 1 1024 1 10 0 1)
                    SF-ADJUSTMENT   _"# of vertical frames"   '(2 1 1024 1 10 0 1)
                    SF-TOGGLE       _"Generate LSL script?"   TRUE
                    SF-STRING       _"Face number (ALL__SIDES for all) (for script):" "ALL_SIDES"
                    SF-VALUE        _"Frames per second (for script):" "10.0")

Change Log

  • Version 2.0.3 (2015-08-08):
    • Internal fixes (properly isolate locals internally)
  • Version 2.0.2 (2015-08-05):
    • Make locals be really local.
    • The generated script did not compile because it lacked the sides parameter. Fixed.
  • Version 2.0.1 (2012-06-05):
    • Disable undo instead of creating an undo group. Makes more sense to the user and saves memory.
  • Version 2.0 (2010-12-22):
    • Frames are no longer required to be the same size as the image.
    • Recognize "combine" layers. Should work with all animated GIFs now.
    • Option to generate the LSL script, active by default.
    • Deal with alpha, so that it's removed if not needed and vice versa.
    • Added progress indicator.
    • Extensively commented.
  • Version 1.0 (2010-12-04): First public release.

See also User:Pedro Oval/GIMP Layers to SL Animated Texture for the (possibly unstable) current development version and future plans.


Written by Pedro Oval. Donated to the public domain.