Shtille's blog A development blog

Making animated avatar for Hodor Reflexes in ESO

Once I’ve decided to make an animated avatar for ESO’s Hodor Reflexes Add-on.

Requirements

The avatar requirements are:

  • size 32x32
  • no more than 50 frames
  • sprite sheet in .dds format

The process

Source selection

I concluded that the most fitting avatar and the most I’ve wanted is one from computer game “Super Meat Boy”.

Video recording

I’ve recorded a video with Geforce Experience tool.

Cropping video

The captured video has format 1920x1080, so I cropped it with ffmpeg into region 80x80:

ffmpeg -i source.mp4 -vf "crop=80:80:925:692" cropped.mp4

The crop region was taken from GIMP by editing one frame image.

Split video into frames

Next step is splitting video into frames.

ffmpeg -i cropped.mp4 frames/img%04d.png

The provided video has been split into 115 frames.

Determine unique frames

Then we need to determine unique frames. No wonder that many frames just coincide with siblings. So unique frame numbers are:

numbers=[1,4,8,12,16,22,26,30,34,38,42,48,52,56,60,64,68,74,78,82,86,90,94,100,104,108,112]

As we can see it’s not so many unique frames.

Determine key frames

There are three key frames for this animation:

1) Left leg forward

2) Stand still

3) Right leg forward

So let’s find out frame numbers with equal positions:

  • left: 8,60,112
  • stand: 16,42,68,94
  • right: 34,86

As we can see, full animation period is 52 frames.

Isolating unique frames

Next we gonna isolate unique cropped frames with numbers from 8 to 60 into separate folder.

numbers=[8,12,16,22,26,30,34,38,42,48,52,56]

So there are 12 unique frames. Store in separate directory:

import shutil

numbers = [8,12,16,22,26,30,34,38,42,48,52,56]
src_format = "./crop/frames/img{number:04d}.png"
dst_format = "./unique/img{number:02d}.png"

i = 0
for x in numbers:
	src = src_format.format(number = x)
	dst = dst_format.format(number = i)
	shutil.copy2(src, dst)
	i += 1

Remove background

We gonna remove background (make it transparent) for each frame. At first copy unique frames to separate directory.

import shutil

src_format = "./unique/img{number:02d}.png"
dst_format = "./transparent/img{number:02d}.png"

for i in range(12):
	src = src_format.format(number = i)
	dst = dst_format.format(number = i)
	shutil.copy2(src, dst)

Then remove background for each frame in GIMP editor.

Consolidate frames

Next we gonna fill missing frames as they were before unique frames isolation. Note that stand key frames have 6 frames gap to the next unique frame instead of regular 4 frames.

import shutil

numbers = [8,12,16,22,26,30,34,38,42,48,52,56]
stand = [16,42]
stand_indices = []

def get_stand_indices():
	for s in stand:
		stand_indices.append(numbers.index(s))

get_stand_indices()

# Determines if this index corresponds to stand key frame
def is_stand(index):
	for i in stand_indices:
		if index == i:
			return True
	return False

src_format = "./transparent/img{number:02d}.png"
dst_format = "./solid/img{number:02d}.png"

# We fill from 8 to 60
index = 0
for i in range(len(numbers)):
	src = src_format.format(number = i)
	count = 4
	if is_stand(i):
		count = 6
	for j in range(count):
		dst = dst_format.format(number = index)
		index += 1
		shutil.copy2(src, dst)
# End loop
src = src_format.format(number = 0)
dst = dst_format.format(number = 52) # = 60 - 8
shutil.copy2(src, dst)

Make GIF from images

At first make original size GIF:

src="./solid/img%02d.png"
dst="avatar80.gif"
palette="/tmp/palette80.png"
filters="fps=30"
framerate=60

ffmpeg -framerate $framerate -i $src -vf "$filters,palettegen" -y $palette
ffmpeg -framerate $framerate -i $src -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -loop 0 -y $dst

The result will be:

Then make GIF 32x32 as required:

src="./solid/img%02d.png"
dst="avatar32.gif"
palette="/tmp/palette32.png"
filters="fps=30,scale=32:-1:flags=lanczos"
framerate=60

ffmpeg -framerate $framerate -i $src -vf "$filters,palettegen" -y $palette
ffmpeg -framerate $framerate -i $src -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -loop 0 -y $dst

The result will be:

Convert GIF to DDS sprite sheet

The easiest solution would be using ImageMagick:

magick -format dds -define dds:compression=none avatar32.gif avatar32.dds

But the result is just one image, not the series of sprites. And here comes the internet solution: ezgif. The spite sheet will be:

And finally we can convert the sprite sheet to desired DDS format via ImageMagick.

Consclusion

So we made a GIF with 27 frames: