2d-модель скелета персонажа (рис. 1) можно представить в виде однонаправленного графа-дерева (рис. 2).
Рис. 1. Скелет персонажа
Рис. 2. Представление скелета персонажа в виде однонаправленного графа-дерева
Такой граф можно расположить на 4-х ярусах (рис. 3).
Рис. 3. Ярусное представление модели
В работе решаются две следующие задачи:
После ввода данных имеющимися в классе model средствами воспроизводится модель и ее анимация. Для вывода изображения использована графическая библиотека OpenGL.
Выделим в модели корневую точку (корень) - вершину на первом ярусе. Иерархию вершин определим в терминах предок - потомок (соответственно начальная и конечная точки ребра ориентированного графа).
Ограничения модели:
Основным элементом языка является описатель.
Определим описатель как структуру, имеющую голову и тело.
Голова описателя состоит из имени и атрибутов, адресующих к элементу модели.
Тело описателя содержит значения (параметры), позволяющие отобразить на плоскости элемент модели.
Тело от головы будем отделять двоеточием, параметры будем разделять запятой, точка с запятой будет завершать тело описателя.
Введем для задания корневой точки описатель knot, имеющий 2 параметра - координаты х, у корня в глобальной системе координат.
Ребра графа представляет описатель line.
Рис. 4. Представление ребра в глобальной системе координат
Определим в описателе 2 атрибута - номера вершин ребра.
Таким образом, голова описателя принимает следующий вид: line [k][i].
Тело описателя содержит 2 параметра - угол наклона fi ребра и длину ребра h.
Пример описателя line.
line [1][2]:fi=30, h=4;
Введенными описателями knot и line можно только задать объект и его начальное положение.
Описатель next задает положение объекта через заданный промежуток времени, который определяется как атрибут описателя. Параметры описателя - это новые значения либо углов ребер, либо координат корневой точки. Каждый описатель next определяет один кадр анимации.
Замечание. Описатели next следуют после представления графа модели описателями knot и line.
Используется нотация Бекуса-Наура.
<модель>::= <описание структуры объекта> | <описание структуры объекта> <описание движения>
<описание структуры объекта>::=<описатель line> | <описатель knot> | <описатель line><описание структуры объекта> | <описатель knot><описатель структуры объекта>
<описание движения>::= <описатель next> | <описатель nехt>|<описание движения>
<описатель>::= <голова>:<тело>
<голова>::= <имя><атрибуты>
<атрибуты>::=NULL | [<число>] | [<число>]<атрибуты>
<число>::=<знак><беззаковое число>
<знак>::=NULL | -
<беззнаковое число>::=<целое> | <дробное>
<целое>::= <цифра> | <цифра><целое>
<дробное>::=<целое>.<целое>
<имя>::= <символ> | <символ><имя> | <имя><число>
<символ> ::= А | ... | Z | а | ... | z
<цифра> ::= 0 | ... | 9
<тело>::= <параметр>; | <параметр>,<тело>
<параметр>::= <имя><атрибут>=<число>
Служебные символы:
: - разделяет имя описателя и тело описателя;
, - разделяет параметры описателя;
; - окончание описателя;
= - присваивает параметру значение;
[,] - в квадратных скобках указываются атрибуты для описателя или параметра.
Описатели:
Описатель | Характеристика |
---|---|
knot |
|
line |
|
next |
|
Пример 1. Неподвижная фигура персонажа.
knot: у=0, х=-5;
line[0][1]: h=2, fi=90;
line[0][2]: h=2, fi=-90;
line[1][3]: h=6, fi=-60;
line[1][4]: h=6, fi=-120;
line[1][5j: h=3, fi=90;
line[2][6]: h=4, fi=-100;
line[2][7]: h=4, fi=-50;
line[6][8]: h=4, fi=-120;
line[7][9]: h=4, fi=-70;
Пример 2. Одно движение персонажа (задаются начальный и последующий кадры).
line[0][1]: h=2, fi=90;
line[0][2]: h=2, fi=-90;
line[1][3]: h=6, fi=-60;
line[1][4]: h=6, fi=-120;
line[1][5]: h=3, fi=90;
line[2][6]: h=4, fi=-100;
line[2][7]: h=4, fi=-50;
line[6][8]: h=4, fi=-120;
line[7][9]: h=4, fi=-70;
knot: y=0, x=-5;
next[0.5]: x=-3, y=0.5, fi[6]=-110, fi[7]=-80, fi[8]=-130, fi[9]=-110;
Класс translator позволяет заполнять данные класса model.
class translator: public model {
FILE *fin;
string str;
char ch;
void error(int nom);
bool read();
bool sifr(bool p,bool f);
bool knot();
bool line(int i,int j);
bool TV(float time);
public:
bool input_tip(FILE*fin1);
};
fin - текстовый файл, содержащий описание модели:
str - строка, предназначенная для хранения очередного считанного слова;
ch - очередной считанный символ.
Методы класса:
error(int nom) - выдает сообщение об ошибке, где nom - номер ошибки;
read() - считывает слово;
sifr(bool p.bool f) - проверяет, является ли str представлением числа, заданного формата, за формат отвечают входные параметры: если p=false, то число положительное, если f=false, то число целое, иначе оно может быть дробным;
knot() - читает тело описателя knot;
line(int i.int j) - читает тело описателя line со входными параметрами i и j;
TV(float time) - читает тело описателя next, со входным параметром time;
input_tip(FILE *fin1) - ввод данных модели из файла описания (функция, выполняющая роль транслятора между языком описания модели и классом model).
void translator::error(int nom) {
char * err[] = {
"синтаксическая ошибка",
"используется неизвестный параметр",
"неверный формат числа",
"ошибка в описании модели",
};
ShowMessage(err[nom]);
}
bool translator::read() {
str="";
do
fscanf(fin,"%c",&ch);
while (((ch==' ',)||(ch=='\n'))&&(!(feof(fin))));
if(feof(fin)) goto A;
while((ch!=':')&&(ch!='[')&&(ch!=']')&&(ch!='=')&&(ch!=',')&&(ch!=';')&&(ch!=' ')&&(ch!='\n')) {
str+=ch;
fscanf(fin,"%c",&ch);
}
if(ch==' ') while (ch=='') fscanf(fin,"%c",&ch);
A:
if(str!="")return true;
else return false;
}
bool translator::sifr(bool p, bool f) {
bool flag=true;
char c;
int cint,n=str.length();
for(int i=0;i<n;i++) {
c=str.c_str()[i];
cint=c;
if ((cint<48)||(cint<57)) {
if((c=='-')&&(i==0)&&p)p=false;
else if((c=='.')&&f) {
str.begin()[i]=',';
f=false;
}
else {
flag=false;
break;
}
}
}
return flag;
}
bool translator::knot() {
bool flag=false;
do {
if (read()&&(ch=='='))
if (str=="x") {
if(read()&&((ch==',')"||(ch==';'))) {
if(sifr(true,true)) {
x0=StrToFloat(str.c_str());
A[0].x=x0;
}
else {error(2);break;}
if (ch==';') flag=true;
}
else {error(0);break;}
}
else if (str=="y") {
if((read())&&((ch==',')||(ch==';'))) {
if(sifr(true,true)) {
y0=StrToFloat(str.c_str());
A[0].y=y0;
}
else {error(2);break;}
if (ch==';') flag=true;
}
else {error(0);break;}
}
else {error(1);break;}
else {error(0);break;}
} while(ch!=';');
return flag;
}
bool translator::line(int i,int j) {
bool flag=false;
A[j].last=&A[i];
if(j<=n)n=j+1;
int nom=0,r=0;
for(;r<=i;r++) nom+=p[r];
int nom2=nom:
for(;r<n;r++) nom2+=p[r];
for(r=nom2;r<nom:r-) q[r]=q[r-1];
q[nom]=j;
p[i]++;
do {
if ((read())&&(ch=='='))
if (str=="h") {
if((read())&&((ch==',')||(ch==';'))) {
if(sifr(false,true)) A[j].h=StrToFloat(str.c_str());
else {error(2);break;}
if (ch==';') flag=true;
}
else {error(0); break;}
}
else if (str=="fi") {
if((read())&&((ch==',')ll(ch==';'))) {
if(sifr(true, false)) {
fi0[j]=StrToFloat(str.c_str())/180;
while (fi0[j]<1)fi0[j]-=2;
while (fi0[j]<-1)fi0[j]+=2;
A[j].fi=fi0[j];
}
else {error(2);break;}
if (ch==';') flag=true;
}
else {error(0);break;}
}
else {error(1);break;}
else {error(0); break;}
} while(ch!=';');
return flag;
}
bool translator::TV(float time) {
bool flag=false; float SS,xx=0,yy=0;
float VV[lis];
int i;
for(i=0;i<lis;i++)VV[i]=0;
do {
if(read())
if (ch=='[')
if (str=="fi") {
if((ch==T)&&(read())&&(ch==']'))
if(sifr(false,false)) {
i=StrToInt(str.c_str());
if((!(read()))&&(ch=='=')&&(read())&&((ch==',')||(ch==';'))) {
if(sifr(true,false)) {
VV[i-1 ]=StrToFloat(str.c_str())/180;
while (VV[i-1]<1) VV[i-1]-=2;
while (VV[i-1]<-1) VV[i-1]+=2;
if(abs(SS=VV[i-1]-A[i].fi)<1)
if(SS<0) SS=1-SS;
else SS=-1*(SS+1);
A[i].fi=VV[i-1];
VV[i-1]=SS/time;
}
else {error(2); break;}
if (ch==';') flag=true;
}
else {error(0); break;}
}
else {error(2);break;}
else {error(0);break;}
}
else {error(0); break;}
else if (ch=='=')
if (str=="x") {
if((read())&&((ch==',')||(ch==';'))) {
if(stfr(true,true)) {
xx=StrToFloat(str.c_str());
xx=(xx-A[0].x)/time;
A[0].x=StrToFloat(str.c_str());
}
else {error(2);break;}
if (ch==V) flag=true;
}
else {error(0);break;}
}
else if (str=="y") {
if((read())&&((ch==',')||(ch==';'))) {
if(sifr(true,true)) {
yy=StrToFloat(str.c_str());
yy=(yy-A[0].y)/time;
A[0].y=StrToFloat(str.c_str());
}
else {error(2);break;}
if (ch==';') flag=true;
}
else {еггог(0); break;}
}
else {error{1);break;}
else (error(O);break;}
else {error(O); break;}
} while(ch !=';');
if (k==0) video.creat(time,xx,yy,VV);
else video.add(time,xx,yy,VV);
k++;
return flag;
}
bool translator::input_tip(FILE *fin1) {
model();
float t;
int i,j;
fin=fin1;
n=1;
do {
if(read())
if(str=="knot")
if(ch==':')
if (knot()) continue; else break;
else {error(0); break;}
else if(str=="line")
if((ch=='[')&&(read())&&(ch==']')
if(sifr(false,false)) {
i=StrToInt(str.c_str());
if((!(read()))&&(ch=='[')&&(read())&&(ch==']'))
if(sifr(false,false)) {
j=StrToint(str.c_str());
if((!(read()))&&(ch==':'))
if(i<j)
if (line(j,i)) continue; else break;
else if (i<j)
if (line(i,j)) continue; else break;
else {error(3);break;}
else {error(0);break;}
}
else {error(2);break;}
else {error(0);break;}
}
else {error(2);break;}
else {error(0);break;}
else if(str=="next")
if((ch=='[')&&(read())&&(ch==']'))
if(sifr(false,true)) {
t=StrToFloat(str.c_str());
if((!(read()))&&(ch==':'))
if (TV(t) continue;
else break;
else {error(0);break;}
}
else {error(2);break;}
else {error(0); break;}
else {error(1);break;}
else if(!(feof(fin))) {error(0);break;}
}while (!(feof(fin)));
last0();
if(feof(fin)) return true;
else return false;
}
Среда реализации : Borland С++ Builder.
При описании модели использованы следующие классы:
class knot {
public:
float x,y;
float h;
float fi;
knot last;
knot();
float gox();
float goy();
};
x,y - координаты вершины (в глобальной системе);
h - длинна ребра, соединяющего вершину с ее предком;
fi - угол наклона ребра (см. рис. 4);
last - указатель на предка.
Методы класса:
knot() - конструктор класса;
gox() - функция возвращает значение координаты х;
goу() - функция возвращает значение координаты у.
class listTV {
public:
float t;
float dx, dy;
float V[lis];
listTV * next;
void creat(float tt, float xx, float yy, float VV[lis]);
void onround(listTV * A);
void offround(int k);
void add(float tt, float xx,float yy, float VV[lis]);
};
Класс представляет собой очередь, где:
t - время составного движения;
dx, dy - проекции скорости корневой точки на ОХ и OY соответственно;
V- вектор скоростей изменения угла fi в течение времени t;
next - указатель на следующее движение;
Методы класса:
creat(float tt, float xx, float yy, float VV[lis]) - создание первого движения (первого элемента очереди), где tt - время движения,
хх, уу - проекции скорости корневой точки, VV - вектор скоростей изменения угла fi;
add(float tt, float xx, float yy, float W[lis]) - добавление движения; входные данные аналогичны данным предыдущей функции;
onround(listTV * А) - устанавливает указатель последнего движения на А, с помощью этой функции можно "зациклить" процесс движения (в примерах будет использовано в для воспроизведения движения);
offround(int к) - устанавливает указатель k-го элемента на NULL, будет использована для разрыва цикла.
class model {
float t; public:
float dt;
int n;
int p [lis];
int q [lis];
knot A[lis];
float x0.y0;
float fi0 [lis];
int k;
listTV *poz;
listTV video;
model();
void last0();
void box();
void next();
bool input(FILE *fin);
void output(FILE *fin);
};
t - текущее время движения;
dt - интервал времени между прорисовкой кадров;
n - количество вершин модели;
{р, q} - MFO-представление графа;
А - вектор вершин модели;
x0.у0 - начальные координаты узла;
fi0 - начальные углы наклона ребер (задают начальное положение модели);
k - количество простых движений;
poz - указатель на текущее движение;
video - очередь, представляющая движение;
Методы класса:
model() - конструктор класса;
last0() - задает начальное состояние модели ;
bох() - прорисовка модели;
next() - вычисление следующего положения модели;
input(FILE *fin) - ввод описания модели из файла;
output(FILE *fin) - создание файла с описанием модели.
knot::knot() {
х=0; У=0; h=0; fi=0;
last=NULL;
}
float knot::gox() {
return (float) h*cos(pi*fi)+last-<x;
}
float knot::goy() {
return (float) h*sin(pi*fi)+last-<y;
}
void listTV::creat(float tt, float xx, float yy, float VV[lis]) {
next=NULL;
t=tt;
dx=xx;
dy=yy;
for(int i=0;i<lis;i++) V[i]=VV[i];
}
void listTV::onround(listTV * A) {
if (next==NULL) next=A; else next-<onround(A);
}
void listTV: :offround(int A) {
if (A==1) next=NULL; else next-<offround(A-1);
}
void listTV: :add(float tt, float xx, float yy, float VV[lis]) {
if (next==NULL) {
next= new listTV;
next-<creat(tt,xx,yy,VV);
}
else next-<add(tt,xx,yy,VV);
}
model::model() {
x0=0; y0=0;
for(int i=0;i<lis;i++) {
fiO[i]=0;
p[i]=0;
q[i]=0;
}
n=0; k=0;
poz=&video;
t=0;
dt=0.01;
}
void model::last0() {
A[0].x=x0;
A[0].y=y0;
for (int i=1;i<n;i++) {
A[i].fi=fiO[i];
A[i].x=A[i].gox();
A[i].y=A[i].goy();
}
poz=&video;
t=0;
}
bool model::input(FILE *fin) {
int i;
fscanf(fin,"%d",&n);
for(i=0;i<n;i++) fscanf(fin,"%d",&p[i]);
for(i=0;i<n-1;i++) fscanf(fin,"%d",&q[i]);
for(i=1;i<n;i++) fscanf(fin,"%f',&A[i].h);
fscanf(fin,"%f',&x0);
fscanf(fin,"%f',&y0);
for(i=1;i<n;i++) fscanf(fin,"%f",&fi0[i]);
int m,j,w=0;
for(i=0;i<n;i++)
for(j=0;j<p[i];j++) {
m=q[w++]; A[m].last=&A[i];
}
fscanf(fin,"%d",&k);
float tt,xx,yy;
float VV[lis];
fscanf(fin,"%f,&tt);
fscanf(fin,"%f',&xx);
fscanf(fin,"%f',&yy);
for(i=0;i<n-1;i++) fscanf(fin,"%f',&W[i]);
video.creat(tt,xx,yy,W);
for(int j=1;j<k;j++) {
fscanf(fin,"%f'.&tt);
fscanf(fin,"%f'.&xx);
fscanf(fin,H%f",&yy);
for(i=0;i<n-1 ;i++) fscanf(fin,"%f',&W[i]);
video.add(tt,xx,yy,VV);
}
last0();
return true;
}
void model::output(FILE *fin) {
int i;
fprintf(fin,"%d \n",n);
for{i=0;i<n;i++) fprintf(fin,"%d ",p[i]);
fprintf(fin,"\n");
for(i=0;i<n-1;i++) fprintf(fin;"%d ",qrj]);
fprintf(fin,"\n");
for(i=1;i<n;i++) fprintf(fin,"%f",A[i].h);
fprintf(fin,"\n");
fprintf(fin,"%f %f \n",x0,y0);
for(i=1;i<n;i++) fprintf(fin,"%f ",fiO[i]);
fprintf(fin,"\n");
fprintf(fin,"%d \n",k);
listTV *poz1;
poz1=&video;
int j=1;
while((poz1 !=NULL)&&(j<=k)) {
fprintf(fin,"%f ",poz1-<t);
fprintf(fin,"%f ",poz1-<dx);
fprintf(fin,"%f ",poz1-<dy);
for(i=0;i<n-1;i++) fprintf(fin,"%f ",poz1-< fprintf(fin,"\n");
poz1=poz1-<next;
}
}
void model::box() {
int i,j,k=0;
float red=1, grin=0;
for(i=0;i<n;i++)
for(j=0;j<p[i];j++,k++) {
glColor3f(red,grin,0);
glBegin (GL_LINES);
glVertex3f (A[q[k]].x,A[q[k]].y,0);
glVertex3f (A[i].x,A[i].y,0);
glEnd();
red+=grin;
grin=red-grin;
red-=grin;
}
}
void model::next() {
t+=dt;
if ((poz!=NULL)&&(poz-<t<t)) {
t=0;
poz=poz-<next;
}
if (pozi=NULL) {
A[0].x+=dt*poz-<dx;
A[0].y+=dt*poz-<dy;
for(int i=1; i<n;i++) {
A[i].fi+=dt*poz-<V[i-1];
A[i].x=A[i].gox();
A[i].y=A[i].goy();
}
}
}
Возьмем следующее описание модели персонажа и его движения:
knot: х = -5;
line[0][1]: h=2, fi=90;
line[0][2]: h=2, fi=-90;
line[1][3]: h=6, fi=-60;
line[1][4]: h=6, fi=-120;
line[1][5]: h=3, fi=90;
line[2][6]: h=4, fi=-100;
line[2][7]: h=4, fi=-50;
line[6][8]: h=4, fi=-120;
line[7][9]: h=4, fi=-70;
next[0.5]:
x=-3, y=0.5,
fi[6]=-110,
fi[7]=-80,
fi[8]=-130,
fi[9]=-110;
next[0.6]:
fi[7]=-100,
fi[6]=-50,
fi[9]=-120,
fi[8]=-70;
next[0.5]:
x=5,
y=0.5,
fi[7]=-110,
fi[6]=-80,
fi[9]=-130,
fi[8]=-110;
next[0.6]:
x=7,
fi[6]=-100,
fi[7]=-50,
fi[8]=-120,
fi[9]=-70;
Созданное приложение обеспечивает чтение файла описания модели и адекватное воспроизведение анимации (все части модели движутся неразрывно).
На рис. 5 приведены кадры анимации персонажа в различные моменты времени.
Рис. 5. Три кадра анимации