10
\$\begingroup\$

I'm trying to get character skinning working on Android.

The idea is quite vanilla: I have my skinning matrices, and along with each vertex, I send up to four matrix indices and four corresponding weights. I sum them in the vertex shader, and apply them to each vertex.

This is what I'm doing in the vertex shader in the iOS version of my game (don't mind the normals):

attribute vec4 in_pos;
attribute vec4 in_normal;
attribute vec2 in_texture_coords;
attribute vec4 in_bone_index;
attribute vec4 in_bone_weight;

varying vec2 fs_texture_coords;

uniform mat4 world_view_projection;
uniform mat4 bones[@bind_matrix_count];

void main()
{
    // Skinning
    vec4 transformed_pos =
        ((in_pos * bones[int(in_bone_index.x)]) * in_bone_weight.x) +
        ((in_pos * bones[int(in_bone_index.y)]) * in_bone_weight.y) +
        ((in_pos * bones[int(in_bone_index.z)]) * in_bone_weight.z) +
        ((in_pos * bones[int(in_bone_index.w)]) * in_bone_weight.w);

    gl_Position = world_view_projection * transformed_pos;
    fs_texture_coords = in_texture_coords;
}

And it works pretty well. However, with the same code in Android, in some devices (notably the Nexus 7 2013), you can't access uniforms with non-constant indices. In other terms, you can't do this:

bones[int(in_bone_index.w)]

because bones[some_non_constant] is always evaluated as bones[0], which is not amusing at all. The worst thing is that the shader compiler happily compiles this.

This guy seemed to have exactly the same problem. He solved it by accessing the uniforms as vectors instead of matrices. I did the same, and in fact it worked!

attribute vec4 in_pos;
attribute vec4 in_normal;
attribute vec2 in_texture_coords;
attribute vec4 in_bone_index;
attribute vec4 in_bone_weight;

varying vec2 fs_texture_coords;

uniform mat4 world_view_projection;
uniform vec4 bones[@bind_matrix_count * 4]; // four vec4's for each matrix

void main()
{
    // Skinning
    mat4 skin_0 = mat4(
        bones[4 * int(in_bone_index.x) + 0],
        bones[4 * int(in_bone_index.x) + 1],
        bones[4 * int(in_bone_index.x) + 2],
        bones[4 * int(in_bone_index.x) + 3]);
    mat4 skin_1 = mat4(
        bones[4 * int(in_bone_index.y) + 0],
        bones[4 * int(in_bone_index.y) + 1],
        bones[4 * int(in_bone_index.y) + 2],
        bones[4 * int(in_bone_index.y) + 3]);
    mat4 skin_2 = mat4(
        bones[4 * int(in_bone_index.z) + 0],
        bones[4 * int(in_bone_index.z) + 1],
        bones[4 * int(in_bone_index.z) + 2],
        bones[4 * int(in_bone_index.z) + 3]);
    mat4 skin_3 = mat4(
        bones[4 * int(in_bone_index.w) + 0],
        bones[4 * int(in_bone_index.w) + 1],
        bones[4 * int(in_bone_index.w) + 2],
        bones[4 * int(in_bone_index.w) + 3]);
    vec4 transformed_pos =
        ((in_pos * skin_0) * in_bone_weight.x) +
        ((in_pos * skin_1) * in_bone_weight.y) +
        ((in_pos * skin_2) * in_bone_weight.z) +
        ((in_pos * skin_3) * in_bone_weight.w);

    gl_Position = world_view_projection * transformed_pos;
    fs_texture_coords = in_texture_coords;
}

But I think this worked as chance. uniforms are not meant to be randomly accessed, so I fear this "technique" will not work in every device.

This guy is passing his matrices as textures, which is a pretty cool idea. I made a 4x32 OES_texture_float texture, where each texel is a matrix row, and each texture row is an entire matrix. I access it like this:

attribute vec4 in_pos;
attribute vec4 in_normal;
attribute vec2 in_texture_coords;
attribute vec4 in_bone_index;
attribute vec4 in_bone_weight;

varying vec2 fs_texture_coords;

uniform mat4 world_view_projection; // A texture!
uniform sampler2D bones;

void main()
{
    // Skinning
    mat4 bone0 = mat4(
        texture2D(bones, vec2(0.00, in_bone_index.x / 32.0)),
        texture2D(bones, vec2(0.25, in_bone_index.x / 32.0)),
        texture2D(bones, vec2(0.50, in_bone_index.x / 32.0)),
        texture2D(bones, vec2(0.75, in_bone_index.x / 32.0)));
    mat4 bone1 = mat4(
        texture2D(bones, vec2(0.00, in_bone_index.y / 32.0)),
        texture2D(bones, vec2(0.25, in_bone_index.y / 32.0)),
        texture2D(bones, vec2(0.50, in_bone_index.y / 32.0)),
        texture2D(bones, vec2(0.75, in_bone_index.y / 32.0)));
    mat4 bone2 = mat4(
        texture2D(bones, vec2(0.00, in_bone_index.z / 32.0)),
        texture2D(bones, vec2(0.25, in_bone_index.z / 32.0)),
        texture2D(bones, vec2(0.50, in_bone_index.z / 32.0)),
        texture2D(bones, vec2(0.75, in_bone_index.z / 32.0)));
    mat4 bone3 = mat4(
        texture2D(bones, vec2(0.00, in_bone_index.w / 32.0)),
        texture2D(bones, vec2(0.25, in_bone_index.w / 32.0)),
        texture2D(bones, vec2(0.50, in_bone_index.w / 32.0)),
        texture2D(bones, vec2(0.75, in_bone_index.w / 32.0)));
    vec4 transformed_pos =
        ((in_pos * bone0) * in_bone_weight.x) +
        ((in_pos * bone1) * in_bone_weight.y) +
        ((in_pos * bone2) * in_bone_weight.z) +
        ((in_pos * bone3) * in_bone_weight.w);

    gl_Position = world_view_projection * transformed_pos;
    fs_texture_coords = in_texture_coords;
}

In fact, this worked pretty nice... Until I tried it on my Galaxy Note 2. This time the compiler complained that I can't use texture2D on the vertex shader!

So what I'm doing is checking whether the GPU supports texture accesses on the vertex shader and if it supports OES_texture_float. If it does, I'm using the texture approach. If it doesn't, I'm using the vector approach.

However, the texture approach is not available on all platforms, and the vector approach is kinda working by chance. I would like to know if there is a way to pass my skinning matrices to the vertex shader, which reliably works on all devices.

I can have minimum reasonable OS requirements, like Android 4.1+, but I would like to have a solution that works on all devices which meet those requirements.

\$\endgroup\$
  • \$\begingroup\$ Well, I can't think of alternatives, TBH I think your best bet is to use both techniques depending of which one is available, if neither are avaible don't use GPU skinning just fallback to CPU skinning implementation (probably with less detailed models?). \$\endgroup\$ – concept3d Jun 18 '14 at 11:32
  • \$\begingroup\$ @concept3d: I don't know, maybe some extension that is guaranteed to exist on android 4.1+ which is designed to solve this problem, or doing something conceptually different with the same results. I consider myself quite proficient in the general concepts of a lot of topics, but my knowledge of the Android platform is very limited, and I was thinking there may be a purely technical workaround to this problem. \$\endgroup\$ – Panda Pajama Jun 18 '14 at 11:45
  • \$\begingroup\$ So maybe this is why I couldn't get skinning to work on my Nexus 7. :/ Thanks, your question opened my eyes! \$\endgroup\$ – async Jun 18 '14 at 17:05
4
\$\begingroup\$

This is non-conforming behaviour by the Nexus 7 (Adreno GPU). You say "uniforms are not meant to be randomly accessed", but according to Appendix A of the spec:

Uniforms (excluding samplers)

In the vertex shader, support for all forms of array indexing is mandated. In the fragment shader, support for indexing is only mandated for constant-index-expressions.

It sounds from the discussion here that this bug applies only to uniform matrix arrays, so the work-around using vectors is likely to work reliably and be portable to other GPUs (I know random uniform indexing works at least on Mali and PowerVR GPUs).

\$\endgroup\$
  • \$\begingroup\$ Hmm, I guess that seems right. I think I had read that thread before, but there is no acknowledgement from Qualcomm confirming this problem. \$\endgroup\$ – Panda Pajama Jun 19 '14 at 4:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.