Плавное изменение [координат] в GLSurfaceView.Renderer
Задача: при нажатии на представление (GLSurfaceView) плавно перемещать объект на новое место.
Объект - экземпляр моего самодельного класса. Объект хранит информацию о своих координатах и размерах (в рендерере они становятся четырьмя точками - вершинами квадрата). Для каждого объекта я создаю отдельный поток, в котором при необходимости плавно изменяются его координаты.
Вопросы: Будет ли лучшим решением создание одной службы, в которой по порядку будут меняться все значения? Почему? Может быть, существует возможность использования какого-нибудь особенного вершинного шейдера?
Для справки: использую android.opengl.GLES20.* и "режим воспроизведения" - RENDERMODE_CONTINUOUSLY.
Renderer
public class OpenGLRenderer implements Renderer {
private Context context;
FloatBuffer vertexData, colorData, objectData;
private int programId, aColorLocation, aPositionLocation, uMatrixLocation;
float[] projectionMatrix = new float[16], viewMatrix = new float[16], modelMatrix = new float[16], matrix = new float[16];
float X = 10f, Y = 10f, Z = 5f, upX = 0f, upY = 1f, upZ = 0, ratio = 1, x, y, z, w, h, s;
int c = 0, cMax = 0, limit = 0, i = 0, mMax = 0, dLimit = 0, m = 0;
float[] vertices = {}, colors = {}, mark = {};
Map<Integer, Float> marks = new HashMap<Integer, Float>();
public OpenGLRenderer(Context context) {
this.context = context;
};
@Override
public void onSurfaceCreated(GL10 arg0, EGLConfig arg1) {
glEnable(GL_DEPTH_TEST);
int vertexShaderId = ShaderUtils.createShader(context, GL_VERTEX_SHADER, R.raw.vertex_shader);
int fragmentShaderId = ShaderUtils.createShader(context, GL_FRAGMENT_SHADER, R.raw.fragment_shader);
programId = ShaderUtils.createProgram(vertexShaderId, fragmentShaderId);
glUseProgram(programId);
Matrix.setIdentityM(modelMatrix, 0);
vertexData = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexData.put(vertices);
colorData = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()).asFloatBuffer();
colorData.put(colors);
objectData = ByteBuffer.allocateDirect(0).order(ByteOrder.nativeOrder()).asFloatBuffer();
getLocations();
};
@Override
public void onSurfaceChanged(GL10 arg0, int width, int height) {
glViewport(0, 0, width, height);
ratio = (float) height / (float) width;
};
private void createMatrix() {
float near = 1f, far = 10f, left = -1f / ratio, right = 1f / ratio, bottom = -1f, top = 1f;
Matrix.frustumM(projectionMatrix, 0, left, right, bottom, top, near, far);
Matrix.setLookAtM(viewMatrix, 0, X, Y, Z, X, Y, Z - 5f, upX, upY, upZ);
Matrix.multiplyMM(matrix, 0, viewMatrix, 0, modelMatrix, 0);
Matrix.multiplyMM(matrix, 0, projectionMatrix, 0, matrix, 0);
};
private void getLocations() {
aColorLocation = glGetAttribLocation(programId, "a_Color");
aPositionLocation = glGetAttribLocation(programId, "a_Position");
uMatrixLocation = glGetUniformLocation(programId, "u_Matrix");
};
private void bindData() {
mMax = 0;
marks.clear();
dLimit = objectData.limit() - limit;
limit = objectData.limit();
cMax = limit / 6;
for (c = 0; c < cMax; c++) {
i = c * 6;
s = objectData.get(i + 5);
if (s == 1) {
mMax++;
x = objectData.get(i);
y = objectData.get(i + 1);
w = objectData.get(i + 3);
h = objectData.get(i + 4);
i = (mMax - 1) * 12;
marks.put(i, x - w);
marks.put(i + 1, y - h);
marks.put(i + 2, 0f);
marks.put(i + 3, x - w);
marks.put(i + 4, y + h);
marks.put(i + 5, 0f);
marks.put(i + 6, x + w);
marks.put(i + 7, y + h);
marks.put(i + 8, 0f);
marks.put(i + 9, x + w);
marks.put(i + 10, y - h);
marks.put(i + 11, 0f);
};
};
m = mMax * 12;
mark = new float[m];
for (c = 0; c < m; c++) {
mark[c] = marks.get(c);
};
vertexData = ByteBuffer.allocateDirect(vertexData.limit() * 4 + dLimit * 8 + m * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
vertexData.position(0);
for (c = 0; c < cMax; c++) {
i = c * 6;
x = objectData.get(i);
y = objectData.get(i + 1);
z = objectData.get(i + 2);
w = objectData.get(i + 3) / 2f;
h = objectData.get(i + 4) / 2f;
vertexData.put(x - w);
vertexData.put(y - h);
vertexData.put(z);
vertexData.put(x - w);
vertexData.put(y + h);
vertexData.put(z);
vertexData.put(x + w);
vertexData.put(y + h);
vertexData.put(z);
vertexData.put(x + w);
vertexData.put(y - h);
vertexData.put(z);
};
vertexData.put(mark);
colorData.position(0);
glVertexAttribPointer(aColorLocation, 3, GL_FLOAT, false, 0, colorData);
glEnableVertexAttribArray(aColorLocation);
vertexData.position(0);
glVertexAttribPointer(aPositionLocation, 3, GL_FLOAT, false, 0, vertexData);
glEnableVertexAttribArray(aPositionLocation);
glUniformMatrix4fv(uMatrixLocation, 1, false, matrix, 0);
};
@Override
public void onDrawFrame(GL10 arg0) {
createMatrix();
bindData();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
for (c = 0; c < cMax; c++) {
glDrawArrays(GL_TRIANGLE_FAN, c*4, 4);
};
glLineWidth(2);
for (c = 0; c < mMax; c++) {
glDrawArrays(GL_LINE_LOOP, (cMax+c)*4, 4);
};
};
Объект
class Unit {
float toX = -1, toY = -1, X = -1, Y = -1, dX = -1, dY = -1, s = -1;
int id = -1;
boolean moving = false;
public Unit(final float[] oldData, final float x, final float y, final float z, final float width, final float height, final boolean isSelected) {
final HandlerThread motionThread = new HandlerThread("com.densityfil.litedwars.MotionTask");
motionThread.start();
final Handler handler = new Handler(motionThread.getLooper());
final Runnable init = new Runnable() {
@Override
public void run() {
final float[] newData = {x, y, z, width, height, isSelected?1:0};
toX = X = x;
toY = Y = y;
id = oldData.length;
renderer.objectData = ByteBuffer.allocateDirect((oldData.length + newData.length) * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
renderer.objectData.position(0);
renderer.objectData.put(oldData);
renderer.objectData.put(newData);
};
};
final Runnable motion = new Runnable() {
@Override
public void run() {
if (moving) {
dX = toX - X;
dY = toY - Y;
s = (float) Math.sqrt(dX * dX + dY * dY);
if (s > 0.007f) {
X += dX / s / 102f;
Y += dY / s / 102f;
renderer.objectData.put(id, X);
renderer.objectData.put(id + 1, Y);
} else moving = false;
};
handler.postDelayed(this, 2);
};
};
handler.post(init);
handler.post(motion);
};
public void moveTo(float denstinationX, float denstinationY) {
toX = denstinationX;
toY = denstinationY;
moving = true;
};
public void setSelected(final boolean isSelected) {
renderer.objectData.put(id + 5, isSelected?1:0);
};
};
Использую в основной деятельности так:
unit.moveTo(x, y);
P.S. будут полезными всякие замечания и комментарии, касающиеся только моего исполнения.
Ответы (1 шт):
Если объекты не поворачиваются вокруг своей оси, только перемещаются, то решить можно очень просто и в лоб. Создавать потоков не придется, воспользуемся классом ValueAnimator. Для этого объекты должны иметь некоторую опорную точку, которую от них можно получить и которую им можно задать. То есть квадрат представляет собой не 4 точки на координатах x1,y1,x2,y2, а одну точку с глобальными координатами x,y и четыремя точками относительно этой опорной. Самый простой вариант - чтобы глобальная тока была в углу, а остальные задавались комбинациями нулей, ширины и высоты объекта. В реальности, конечно, можно хранить все глобальные координаты, а при установке новой опорной точки просто пересчитывать все 4, как это делать уже от вас зависит, но опорная точка должна быть. Получим примерно такой класс, при условии что Vector3f - это внезапно вектор из 3 флоатов, имеющий методы для операций над векторами
private class ToPositionAnimator extends ValueAnimator implements
ValueAnimator.AnimatorUpdateListener {
final private Vector3f startPoint, displacement;
final private UIObject object;
ToStateAnimator(UIObject obj, Vector3f endPoint) {
this.object = obj//передаем в аниматор объект который надо переместить
startPoint = object.getCord(); //запоминаем исходное местоположение
displacement = startPoint.sub(endPoint);//и место, где он должен оказаться
векторно вычитая из конечной точки стартовую
setFloatValues(0f, 1f);//аниматор будет использовать служебное значение, меняющееся от 0 до 1. Грубо говоря это доля пройденного пути в нашем случае будет
setDuration(duration);//задаем длительность в милисекундах
setInterpolator(smth);//интерполятор это штука, которая задает характер анимации. По сути она искажает график зависимости от времени того служебного значения, которое от 0 до 1 меняется. По умолчанию оно линейно, можно ставить экспонентоподобный график, синусоидоподобный и вообще свой собственный сделать, если хочется. Позволяет легко изменить характер движения с равномерного на равноускоренный или сначала равноускоренный, потом немного равномерного и в конце "равнозамедленного". Гугл в помощь, если что
addUpdateListener(this);//задаем слушатель изменений служебного значения
}
//этот метод будет вызываться каждый кадр(системный, а не SurfaceView) в главном потоке со значением служебной переменной в AnimatedValue соответсвующему текущему времени
@Override
public void onAnimationUpdate(ValueAnimator animation) {
final float interpolated_time = (Float) animation.getAnimatedValue();//получаем текущее значение служебной переменной
Vector3f currentPoint = startPoint + displacement.mult(interpolated_time);//умножаем вектор перемещения на "долю пройденого пути" и прибавляем к начальной точке, получаем текущую
object.setCord(currentPoint);//ставим объект в нужную точку
}
}
Теперь чтобы переместить объект в нужную точку делаем
animator = new ToStateAnimator(object, newPoint);
animator.start();
Лучше класть аниматор в переменную, чтобы можно было прервать анимацию в случае чего. Обратите внимание, что start() надо делать из главного потока. И вот с минимумом усилий мы сделали плавное перемещение объекта
Еще можно создать отдельно объект класса ValueAnimator через его статически функции, отдельно слушатель и не создавать свой класс, но мне так было удобнее ориентироваться. Еще есть ObjectAnimator, который может помочь делать некоторые вещи, но им удобно менять значения у привычных андроиду свойств вьюшек, такие как ширина, высота, прозрачность и т.п.(что в xml можно задать как атрибут), потому тут он вряд ли вам поможет
Приведенный мой вариант не допускает запуска нескольких анимаций для одного объекта параллельно, но чтобы это допустить(скажем, одну анимацию ставите для горизонтального перемещения, вторую для вертикального) надо лишь изменить входные данные, и вместо того чтобы прибавлять некоторый вектор к изначальной позиции прибавлять всегда к текущей.
Если объект перемещаться должен за пальцем, то никакие аниматоры не требуются. в OnTochListener на событии ACTION_DOWN запоминаем текущие координаты объекта и координаты "тыка", а на событии ACTION_MOVE сравниваем текущии координаты тыка с запомнеными ранее, получаем вектор перемещения в системе координат экрана, транслируем в координаты мира(операция сильно зависит от того как вы используете пространство, но если вы умеете понимать объект в который ткнули, значит вы знаете как это сделать). Ну и получив транслированный вектор остается лишь арибавить его к начальной позиции объекта и получить текущую. Т.к. события сенсорного экрана вырабатываются каждый кадр, то анимировать это смещение нет необходимости, просто втыкайте новые координаты и все
Ну и если вам требуются сложные перемещения объекта в пространстве, то либо пытайтесь получить его комбинацией таких аниматоров, либо учитесь создавать матрицы преобразований и подтыкать их к вашей MVP в нужный момент при рисовании объекта. Тогда объект будет выглядеть как набор опорная точка + точки в локальных коордантах объекта(от опорной точки) + матрица преобразований для объекта, и точки вообще никогда не пересчитываются, все преобразования закладываются в матрицу и высчитываются каждый кадр на видеочипе