Rémy Mathieu


Bloom effect in Go

Feb 12, 20205 minutes read

Introduction

While working on one of my side-project, I’ve had the need to render a kind of neon or glow effect on images (think the effect used everywhere in Tron). Something very close to the bloom effect used in video-games. I’ve needed to do this using software rendering (computed on the CPU), because it will run on a server without any GPU (note that it must not be confused with the bloom filter data structure).

In reality, the bloom effect is produced by the lens of cameras which can never perfectly focus, and, to cite the Wikipedia article :

Under normal circumstances, these imperfections are not noticeable, but an intensely
  bright light source will cause the imperfections [the bloom] to become visible.

How to replicate the effect

This image is composed of three rendered elements: a gray background, a red rectangle and a red circle.

Basic rendering with no bloom effect

Everything looks flat and a bit boring: for a nice effect, we will want the red rectangle and the red circle to have some bloom effect. We want:

Rendering with bloom effect

In a rendering pipeline, you control what you draw and in which order, it will then be quite easy to apply an effect only on some parts of what you’re rendering and in this case, on the red lines.

The rendering process will be:

  • Create a first buffer representing the empty image
  • Draw the gray background on this buffer (it doesn’t need any processing)
  • Draw the red rectangle and red circle in a second buffer
  • Apply the bloom effect on this second buffer
  • Draw this second buffer on top of the first one

After this, the first buffer should be containing neat red lines with bloom effect on top of a gray background. But in this rendering process, what does Apply the bloom effect mean?

Bloom effect using Go

Graphic libraries

For the rendering, I’ve used the excellent gg library. Drawing lines, rectangles and texts is straight-forward with it.

For the graphic effects, I’ve selected bild which is really easy to use and which I also recommend.

Implementation

Basically, having a camera out of focus means that the captured image is blurry, and we previously said that this is noticeable with bright sources of light.

Then, what we will want to do is to blur the red lines, because here, it is our source of light. After having blurred their lines, we draw them on top of the gray background. Let’s check out the result using the Gaussian blur shipped in the bild library:

Basic rendering with blurred red lines

We immediatly see that it is not really the final effect that we want: the source of light has kind of disappeared, becoming blurry but way darker.

We’ve lost our source of light? OK, let’s draw it again on top of the blurred one:

Basic rendering blurred red lines and original red lines on top of it

We are getting there, however, in this image I think that the effect is… too light. pun

Because we are blurring the red lines, they get transparent around their borders. Drawing the original lines on top helps but how could we increase the amount of light in the blurred one, without having this much transparency? Applying the blur a second time (or increasing its radius) won’t help, it will instead decrease the amount of light by making it sparse.

The solution is to simply use a bigger source of light before applying the blur. In order to have this improved source of light from the rectangle and the circle: we want to dilate them first. Their lines will be bigger and we will then apply a blur on a larger source of light. Hopefully, the bild library is shipping a dilate effect.

Rendering with different colors:

Rendering red with bloom effect Rendering blue with bloom effect Rendering green with bloom effect

At this point, I think it is what we were looking for!

Conclusion

We’ve seen that to imitate the glow or bloom effect on an image by:

  • Dilating the source of light to make it bigger
  • Blurring it to make it look out of focus
  • On top of this blurred light, draw the original one (which is in focus)

Please note that I’m not an expert in image processing, especially not in colors and lights theory. I hope that this article could be helpful to someone else.

Code

Here’s the full code for this article:

package main

import (
	"image"

	"github.com/anthonynsimon/bild/blur"
	"github.com/anthonynsimon/bild/effect"
	"github.com/fogleman/gg"
)

func main() {
	// draw the source of light
	// it's the buffer on which we will apply the bloom effect
	dc := gg.NewContext(200, 200)
	dc.SetLineWidth(3.0)
	dc.SetRGB255(147, 112, 219)
	dc.DrawRectangle(10, 10, 180, 180)
	dc.DrawCircle(100, 100, 50)
	dc.Stroke()

	// store the original source of light to draw it back later
	original := dc.Image()

	// bloom this source of light
	bloomed := Bloom(dc.Image())

	// now, let's do our final rendering, let's starts by rendering
	// a gray rectangle in a new buffer
	dc = gg.NewContext(220, 220)
	dc.SetRGB255(40, 40, 40)
	dc.DrawRectangle(0, 0, 220, 220)
	dc.Fill()

	// draw our bloomed light
	dc.DrawImage(bloomed, 0, 0)

	// re-apply the original source of light
	dc.DrawImage(original, 10, 10)

	// save the result in a PNG
	dc.SavePNG("output.png")
}

// Bloom applies a bloom effect on the given image.
// Because of the nature of the effect, a larger image is returned.
// 10px padding is added to each side of the image, growing it by
// 20px on X and 20px on Y.
func Bloom(img image.Image) image.Image {
	// create a larger image
	size := img.Bounds().Size()
	newSize := image.Rect(0, 0, size.X+20, size.Y+20)

	// copy the original in this larger image, slightly translated to the center
	var extended image.Image
	extended = translateImage(img, newSize, 10, 10)

	// dilate the image to have a bigger source of light
	dilated := effect.Dilate(extended, 3)

	// blur the image
	bloomed := blur.Gaussian(dilated, 10.0)

	return bloomed
}

// translateImage copies the src image applying the given offset on a new Image
// bounds is the size of the resulting image.
func translateImage(src image.Image, bounds image.Rectangle, xOffset, yOffset int) image.Image {
	rv := image.NewRGBA(bounds)
	size := src.Bounds().Size()
	for x := 0; x < size.X; x++ {
		for y := 0; y < size.Y; y++ {
			rv.Set(xOffset+x, yOffset+y, src.At(x, y))
		}
	}
	return rv
}

Back to posts