Once upon a time I wrote C2SpriteManager to solve the problem of cropping sprites and supplying some positional metadata alongside those sprites, most especially their pivot or origin points.
Cropping of sprites is necessary not only for the benefit of in-game rendering, but also to optimise the constant importing of files from the DCC.
The basic issue is as follows, taking this sprite into consideration:
In the raw render, the sprite’s origin point is exactly at the centre of the image (i.e. 05., 0.5).
The reason why the raw render is not pre-cropped is because this is a 3d render scene and the camera setup is templated to render a series of animation.
And the reason why it is not more closely cropped is because it is more efficient to set up a generic “one-size fits all” rendering template with enough canvas room for the character to move, than to discover that it lacks, it and have to apply the template retrospectively or have multiple templates for different characters/props.
That decided, I had to consider the boundary-cropped image:
In a cropped image, the origin is near the bottom (i.e. 0.46,1.008)
And in another frame of the same sequence the origin has changed again (i.e. 0.37, 0.97)
Yet in the raw render, the origin point is consistently at the centre, because the image resolution remains the same.
The problem is that we need to position every sprite frame accurately on the transform we use to move the character, or else it if we left it as they are, the character would be jumping around.
In any 2d rendering engine, sprites are anchored to a reference transform. In Godot and C2, it is the upper-left corner. In Unity, I believe this could be changed to anchor at centre.
But in any case, using anchors will not do. Unless the sprite were close-cropped and had consistent resolution, the relative width and height resolutions will throw off the registration off.
This is where cropping imagepoints come in. In cropping the image, we also crop the imagepoints file. We start simply using the origin, which is always 0.5, 0.5 (centre).
If we determine the boundary (bounding box) of the image, how do we get the centre point using the new boundaries?
canvas_bounding_box = list(image.getbbox())
...
with open(crop_ip_file, 'w') as cimp:
for ln in lines:
line = ln.strip()
ip_name, ip_x, ip_y = line.split('\t')
# convert ip ratio to pixel
spx = float(ip_x) * image_size[0]
spy = float(ip_y) * image_size[1]
diff_w = spx - canvas_bounding_box[0]
diff_h = spy - canvas_bounding_box[1]
new_w = canvas_bounding_box[2] - canvas_bounding_box[0]
new_h = canvas_bounding_box[3] - canvas_bounding_box[1]
new_ratio_x = float(diff_w/new_w)
new_ratio_y = float(diff_h/new_h)
cimp.write(f'{ip_name}\t{new_ratio_x}\t{new_ratio_y}\n')
So by taking an input centre value which is the ratio x and y values of the point on the image, we get new ratio values for the new crop.
But then we need to apply this new ratio during game render, so we have to be able to know the cropped width/height and apply an offset based of the ratio.
var current_frame_width = get_current_frame_width()
var current_frame_height = get_current_frame_height()
hotspot_offset.x = current_frame_width * hotspot_position_ratio[0]
hotspot_offset.y = current_frame_height * hotspot_position_ratio[1]
position.x = -hotspot_offset.x
position.y = -hotspot_offset.y
How to get imagepoint positions in 3d
I can take this further by supplying any point in the image that is significant, and turn it into an actual position in the game engine. For example, I use imagepoint files as a way to determine where the muzzle point of the weapon is, because that’s where I will spawn my bullets.
But then we are headed into a discussion of how to convert a 3d point to screenspace. If you’re looking for someone to explain to you like an 8-year old, you’ve come to the right place, because because that’s the about the level of my maths!
First, I used this as my guide. It was too adult, but by its help and no small miracle, I figured it out, and wanting to spare others from having to grow up mathematically, this is the rundown first and I’ll go into simple details afterwards:
- Get the item’s world position
- Get the camera’s matrix. This is basically the orientation of the camera. I’m using a 3×3 matrix, by the way.
- Do a transform operation (more on that below) on the item’s world position onto the world-to-camera matrix.
Camera matrix
In most 3d apps, you’re able to get the Right, Up, Forward vectors of a given item, which are the vector directions for each of the axis vectors. When I was much younger it was useful to visualise the axis gizmo arrows. For example, the Right vector is where the X axis arrow is pointing to; the Forward vector is where the Z axis arrow is aiming towards.
In LW:
get_item_vmatrix: item
{
vmatrix[1] = item.getWorldRight(Scene().currenttime);
vmatrix[2] = item.getWorldUp(Scene().currenttime);
vmatrix[3] = item.getWorldForward(Scene().currenttime);
return (vmatrix);
}
Transform operation
A transform operation mutiplies the position by the matrix, like this;
transform: a, m
{
for ( i =1; i <= 3; i++ )
{
b[ i ] = a.x * m[ 1 , i ] +
a.y * m[ 2 , i ] +
a.z * m[ 3 , i ];
}
ret = <b[1],b[2],b[3]>;
return(ret);
}
Flattened, it looks like:
res.x = a.x * m[ 1 , 1 ] + a.y * m[ 2 , 1 ] + a.z * m[ 3 , 1 ]
res.y = a.x * m[ 1 , 2 ] + a.y * m[ 2 , 2 ] + a.z * m[ 3 , 2 ]
res.z = a.x * m[ 1 , 3 ] + a.y * m[ 2 , 3 ] + a.z * m[ 3 , 3 ]
Where a
is the original item, and m
is the camera matrix.
Even in simpler terms:
- The new position X is the result of adding these together
- The original item’s position X multiplied by the camera matrix’s Right vector’s X component
- The original item’s position X multiplied by the camera matrix’s Up vector’s X component
- The original item’s position X multiplied by the camera matrix’s Forward vector X component
- In the same way, the new position Y is result of adding:
- The original item’s position Y multiplied by the camera matrix’s Right vector’s Y component
- The original item’s position Y multiplied by the camera matrix’s Up vector’s Y component
- The original item’s position Y multiplied by the camera matrix’s Forward vector Y component
- And I don’t have to spell everything out….
The actual result
What you get is the transformation of that point to camera space, which is all still in 3d, that is, 3d units.
Because I wanted the ratio of that point as seen through the orthographic camera, it was a matter of just getting the resolution and then using my trusty remapping function.
cam_size = find_cam_size(camera)/2;
nmin = 0;
nmax = 1;
omin_x = -cam_size;
omax_x = cam_size;
omin_y = cam_size;
omax_y = -cam_size;
value = pos.x;
result_x = remap(omin_x, omax_x, nmin, nmax, value, nil);
value = pos.y;
result_y = remap(omin_y, omax_y, nmin, nmax, value, nil);
remap: omin, omax, nmin, nmax, value, limit
{
oldrange = omax - omin;
oratio = (value - omin) / oldrange;
newrange = nmax - nmin;
result = (newrange*oratio) + nmin;
// info(result);
if(limit)
{
if(result > nmax)
result = nmax;
else if(result < nmin)
result = nmin;
}
return(result);
}