Отображение трёхмерной графики на PSP

в 10:58, , рубрики: psp, Программирование, старое железо

Пару месяцев назад я вновь достал из ящика запылившуюся PSP и решил портировать туда мой ранее уже показанный движок. С программной отрисовкой проблем не возникло – всё и так работает. А вот с использованием GU всё оказалось не так просто. В данной статье я покажу на примере, как можно написать для PSP простое трёхмерное приложение, использующее GU.

Заранее предупреждаю, что руководств по программированию для PSP довольно мало, и поэтому какие-то мои выводы могут оказаться неверными. Но, к делу.

Главная функция программы для PSP, если кто не знает, оформляется примерно вот так:

#include <pspkernel.h>
#include <pspdebug.h>
#include <pspdisplay.h>

//----------------------------------------------------------------------------------------
PSP_MODULE_INFO("GUTexture", 0, 1, 1);
PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER|THREAD_ATTR_VFPU);

void dump_threadstatus(void);

bool done=false;

int exit_callback(int arg1,int arg2,void *common)
{
 done=true;
 return(0);
}

int CallbackThread(SceSize args, void *argp)
{
 int cbid;
 cbid=sceKernelCreateCallback("Exit Callback",exit_callback,NULL);
 sceKernelRegisterExitCallback(cbid);
 sceKernelSleepThreadCB();
 return(0);
}
int SetupCallbacks(void)
{
 int thid = 0;
 thid=sceKernelCreateThread("update_thread",CallbackThread,0x11,0xFA0,0,0);
 if(thid>=0) sceKernelStartThread(thid, 0, 0);
 return(thid);
}
//----------------------------------------------------------------------------------------
//начинаем программу
//----------------------------------------------------------------------------------------

int main(int argc, char  **argv)
{
 pspDebugScreenInit();
 //устанавливаем обработчики
 SetupCallbacks();
 //выполняем программу 
 ……….
 //выходим из программы
 sceKernelExitGame();
 return(0);
}

Инициализация GU выполняется следующим образом:

Сначала мы запрашиваем указатели на три буфера – экранный, внеэкранный и буфер глубины (Z-буфер). Буферы выравниваются по 512 пикселей в строке (хотя у PSP строка 480 пикселей). Также требуется учесть формат цвета пикселя. В данном примере использован формат GU_PSM_8888 — по 8 бит на R,G,B и Alpha-компоненты цвета пикселя. Для Z-буффера использован формат GU_PSM_4444 просто потому что это 16 бит — Z-буфер у PSP 16 битный.

//размеры экрана
#define SCREEN_WIDTH 480
#define SCREEN_HEIGHT 272
#define SCREEN_LINE_WIDTH 512

void* fbp0=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_8888);
void* fbp1=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_8888);
void* zbp=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_4444);

Функция запроса указателей на буферы определяется как

#include <pspge.h>
#include <pspgu.h>

static unsigned int staticOffset=0;

static unsigned int getMemorySize(unsigned int width,unsigned int height,unsigned int psm)
{
 switch (psm)
 {
  case GU_PSM_T4: return((width*height)>>1);
  case GU_PSM_T8: return(width*height);
  case GU_PSM_5650:
  case GU_PSM_5551:
  case GU_PSM_4444:
  case GU_PSM_T16: return(2*width*height);
  case GU_PSM_8888:
  case GU_PSM_T32: return(4*width*height);
  default: return(0);
 }
}

void* getStaticVramBuffer(unsigned int width,unsigned int height,unsigned int psm)
{
 unsigned int memSize=getMemorySize(width,height,psm);
 void* result=(void*)staticOffset;
 staticOffset+=memSize;
 return(result);
}

void* getStaticVramTexture(unsigned int width,unsigned int height,unsigned int psm)
{
 void* result=getStaticVramBuffer(width,height,psm);
 return((void*)(((unsigned int)result) + ((unsigned int)sceGeEdramGetAddr())));
}

Это не мои функции – я их взял из какой-то программы давным-давно и лишь слегка изменил. Память распределяется в области видеопамяти. Текстуры также следует по возможности размещать там же, запрашивая указатель через getStaticVramTexture, иначе быстродействие резко упадёт. Разумеется, никакой динамической памяти при таких запросах не выделяется, а просто распределяется часть заданного адресного пространства PSP под экран и текстуры. Насколько я помню, видеопамяти у PSP всего 2 мегабайта — это очень мало для хранения множества текстур.

Программирование GU у PSP похоже на программирование для OpenGL с одним отличием — выполнение команд требует их размещения в дисплейном списке, причём память для этого списка должна быть заранее выделена и при этом выровнена:
static unsigned char __attribute__((aligned(16))) DisplayList[262144];
Команды, относящиеся к преобразованию координат не требуют дисплейного списка и могут выполняться в любом месте программы.

Инициализировать GU можно, например, так:

//размер сторон виртуального экрана PSP
#define VIRTUAL_SCREEN_SIZE 2048
//соотношение сторон экрана
#define SCREEN_ASPECT 16.0f/9.0f
//передняя плоскость отсечения
#define NEAR_PLANE_Z 5.0f
//задняя плоскость отсечения
#define FAR_PLANE_Z 4096.0f
//угол зрения
#define EYE_ANGLE 60.0f

//инициализируем графику GU
 sceGuInit();
 //создаём и запускаем на выполнение новый контекст дисплея - он должен выполниться сразу, т.к. GU_DIRECT
 sceGuStart(GU_DIRECT,DisplayList);
 //устанавливаем параметры буфера рисования- формат пикселя, указатель на область видеопамяти, длину строки (выровненную, а не физическую)
 sceGuDrawBuffer(GU_PSM_8888,fbp0,SCREEN_LINE_WIDTH);
 //устанавливаем параметры буфера экрана - размер экрана, указатель на видеопамять, длину строки
 sceGuDispBuffer(SCREEN_WIDTH,SCREEN_HEIGHT,fbp1,SCREEN_LINE_WIDTH);
 //устанавливаем параметры буфера глубины- указатель на начало буфера глубины в видеопамяти и длину строки
 sceGuDepthBuffer(zbp,SCREEN_LINE_WIDTH);
 //устанавливаем смещение экрана в общем пространстве 4096x4096 (в PSP такой размер виртуального экрана)
 sceGuOffset(VIRTUAL_SCREEN_SIZE-(SCREEN_WIDTH/2),VIRTUAL_SCREEN_SIZE-(SCREEN_HEIGHT/2));//ставим по центру
 //настраиваем видовой порт - порт просмотра- координаты центра и размеры сторон
 sceGuViewport(VIRTUAL_SCREEN_SIZE,VIRTUAL_SCREEN_SIZE,SCREEN_WIDTH,SCREEN_HEIGHT);
 //устанавливаем диапазон значений для буфера глубины - передняя и задняя плоскости отсечения (буфер инвертирован и значения от 0 до 65535 !)
 sceGuDepthRange(65535,0);
 //включаем обрезание области показа по размерам видового порта
 sceGuScissor(0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
 sceGuEnable(GU_SCISSOR_TEST);
 sceGuEnable(GU_CLIP_PLANES);
  //настроим матрицу проецирования
 sceGumMatrixMode(GU_PROJECTION);
 sceGumLoadIdentity();
 sceGumPerspective(EYE_ANGLE,SCREEN_ASPECT,NEAR_PLANE_Z,FAR_PLANE_Z);
 //включим режим гладкой интерполяции цвета граней
 sceGuShadeModel(GU_SMOOTH);
 //включим тест глубины
 sceGuDepthFunc(GU_GEQUAL);
 sceGuEnable(GU_DEPTH_TEST);
 sceGuDepthMask(GU_FALSE);
 //отключим режим отсечения граней, повёрнутых обратной стороной к наблюдателю
 sceGuFrontFace(GU_CCW);
 sceGuDisable(GU_CULL_FACE);
 //настраиваем прозрачность
 sceGuDisable(GU_BLEND);
 sceGuBlendFunc(GU_ADD,GU_SRC_ALPHA,GU_ONE_MINUS_SRC_ALPHA,0,0);
 //выполняем созданный список
 sceGuFinish();
 sceGuSync(GU_SYNC_WAIT,GU_SYNC_FINISH);
 sceGuDisplay(GU_TRUE); 

После завершения работы с GU следует вызвать sceGuTerm().

После загрузки текстуры размера (WidthImage; HeightImage) любым удобным способом (указатель Data на данные текстуры – и лучше его получать в области видеопамяти), мы можем её вывести на экран.

  //рисуем сцену
  sceGuStart(GU_DIRECT,DisplayList);
  //очистим экран и буфер глубины
  sceGuClearColor(0);
  sceGuClearDepth(0);
  sceGuClear(GU_COLOR_BUFFER_BIT|GU_DEPTH_BUFFER_BIT);

  //настроим матрицу проецирования
  sceGumMatrixMode(GU_PROJECTION);
  sceGumLoadIdentity();
  sceGumPerspective(EYE_ANGLE,SCREEN_ASPECT,NEAR_PLANE_Z,FAR_PLANE_Z);
  sceGumUpdateMatrix();.

  //инициализируем матрицы
  sceGumMatrixMode(GU_TEXTURE);
  sceGumLoadIdentity();
  sceGumMatrixMode(GU_VIEW);
  sceGumLoadIdentity();
  sceGumMatrixMode(GU_MODEL);
  sceGumLoadIdentity();

  //выводим прямоугольник с текстурой
  sceGuColor(0xffffffff);//цвет окраски
  sceGuEnable(GU_TEXTURE_2D);
  sceGuTexMode(GU_PSM_8888,0,0,0);
  sceGuTexImage(0,WidthImage,HeightImage,WidthImage,Data);
  sceGuTexFunc(GU_TFX_MODULATE,GU_TCC_RGBA);
  sceGuTexFilter(GU_NEAREST,GU_NEAREST);
  sceGuTexWrap(GU_REPEAT,GU_REPEAT);
  sceGuTexScale(1,1);
  sceGuTexOffset(0,0);
 
  //выводим полигон по точкам из массива
  …
    
  sceGuDisable(GU_TEXTURE_2D);
  //запускаем список на выполнение
  sceGuFinish();
  sceGuSync(GU_SYNC_WAIT,GU_SYNC_FINISH);
  //делаем видимым буфер, в котором мы рисовали
  sceDisplayWaitVblankStart();
  sceGuSwapBuffers();

Как вывести полигон? Для рисования геометрии GU у PSP просит поместить все точки в массив, указатель на который нужно предварительно получить командой sceGuGetMemory, передав ей размер запрашиваемого блока памяти в байтах. Дальше по этому указателю вы должны записать массив точек и попросить PSP их вывести, например, командой sceGumDrawArray с нужными параметрами. Но каков формат этих точек? У PSP данные точек располагаются в определённом порядке и размер массива, описывающего одну точку должен быть кратен 32 байтам: Вес вершины, текстурные координаты, цвет точки, нормаль к точке, координата точки. Именно в таком порядке. Чтобы не заморачиваться с форматом, я определил набор структур и функций для работы с ними:

//#pragma pack(1)
//[for vertices(1-8)] [weights (0-8)] [texture uv] [color] [normal] [vertex] [/for]

#pragma pack(1)

//координата точки
struct SGuVertex
{
 float X;
 float Y;
 float Z;
};
//нормаль к точке
struct SGuNormal
{
 float Nx;
 float Ny;
 float Nz;
};
//текстурные координаты
struct SGuTexture
{
 float U;
 float V;
};
//цвет точки
struct SGuColor
{
 unsigned long Color;
};
#pragma pack()

#pragma pack(32)
//точка с текстурой, цветом, нормалью, координатами
struct SGuNVCTPoint
{
 SGuTexture sGuTexture;
 SGuColor sGuColor;
 SGuNormal sGuNormal;
 SGuVertex sGuVertex;
};
#pragma pack()

  void SetVertexCoord(SGuVertex &sGuVertex,float x,float y,float z);//задать координаты вершины
  void SetNormalCoord(SGuNormal &sGuNormal,float nx,float ny,float nz);//задать координаты нормали
  void SetTextureCoord(SGuTexture &sGuTexture,float u,float v);//задать координаты текстуры
  void SetColorValue(SGuColor &sGuColor,unsigned long color);//задать цвет

//----------------------------------------------------------------------------------------------------
//задать координаты вершины
//----------------------------------------------------------------------------------------------------
void CMain::SetVertexCoord(SGuVertex &sGuVertex,float x,float y,float z)
{
 sGuVertex.X=x;
 sGuVertex.Y=y;
 sGuVertex.Z=z;
}
//----------------------------------------------------------------------------------------------------
//задать координаты нормали
//----------------------------------------------------------------------------------------------------
void CMain::SetNormalCoord(SGuNormal &sGuNormal,float nx,float ny,float nz)
{
 sGuNormal.Nx=nx;
 sGuNormal.Ny=ny;
 sGuNormal.Nz=nz;
}
//----------------------------------------------------------------------------------------------------
//задать координаты текстуры
//----------------------------------------------------------------------------------------------------
void CMain::SetTextureCoord(SGuTexture &sGuTexture,float u,float v)
{
 sGuTexture.U=u;
 sGuTexture.V=v;
}
//----------------------------------------------------------------------------------------------------
//задать цвет
//----------------------------------------------------------------------------------------------------
void CMain::SetColorValue(SGuColor &sGuColor,unsigned long color)
{
 sGuColor.Color=color;
}

Тогда задать геометрию (в данном случае — квадрат) можно, например, так:

  //задаём геометрию
  SGuNVCTPoint sGuNVCTPoint;
  vector<SGuNVCTPoint> vector_point;

  SetVertexCoord(sGuNVCTPoint.sGuVertex,-100,100,0);
  SetTextureCoord(sGuNVCTPoint.sGuTexture,0,0);
  SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1);
  SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF);
  vector_point.push_back(sGuNVCTPoint);

  SetVertexCoord(sGuNVCTPoint.sGuVertex,100,100,0);
  SetTextureCoord(sGuNVCTPoint.sGuTexture,1,0);
  SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1);
  SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF);
  vector_point.push_back(sGuNVCTPoint);

  SetVertexCoord(sGuNVCTPoint.sGuVertex,100,-100,0);
  SetTextureCoord(sGuNVCTPoint.sGuTexture,1,1);
  SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1);
  SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF);
  vector_point.push_back(sGuNVCTPoint);

  SetVertexCoord(sGuNVCTPoint.sGuVertex,-100,-100,0);
  SetTextureCoord(sGuNVCTPoint.sGuTexture,0,1);
  SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1);
  SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF);
  vector_point.push_back(sGuNVCTPoint);

А вывести его, например, так:

  size_t vertex_amount=vector_point.size();
  SGuNVCTPoint *sGuNVCTPoint_Ptr=(SGuNVCTPoint*)sceGuGetMemory(vertex_amount*sizeof(SGuNVCTPoint));
  if (sGuNVCTPoint_Ptr!=NULL)
  {
   for(size_t n=0;n<vertex_amount;n++) sGuNVCTPoint_Ptr[n]=vector_point[n];
   sceGumDrawArray(GU_TRIANGLE_FAN,GU_COLOR_8888|GU_VERTEX_32BITF|GU_TRANSFORM_3D|GU_NORMAL_32BITF|GU_TEXTURE_32BITF,vertex_amount,0,sGuNVCTPoint_Ptr);
  }

Для вывода я указал функции sceGumDrawArray, что именно я рисую и каков формат точки ( GU_COLOR_8888|GU_VERTEX_32BITF|GU_TRANSFORM_3D|GU_NORMAL_32BITF|GU_TEXTURE_32BITF — точка состоит из цвета, координат, нормали, текстурных координат и требует перемножения координат на соответствующие матрицы перед рисованием). Рисование возможно только треугольниками. Но это ещё не всё…

Вроде бы всё работает, но работает только, если все точки находятся перед глазами и видимы. Стоит хотя бы одной точки уйти в какую-то туманную даль, как GU отказывается рисовать весь многоугольник. Как я понимаю, GU у PSP требует, чтоб относительно четырёх плоскостей отсечения (левая, правая, верхняя и нижняя (а передняя получится автоматически)) точка лежала внутри этого объёма, иначе GU не согласен её выводить. Проблема. Но в играх-то 3D-графика присутствует и таких артефактов не наблюдается! Давайте посмотрим, как решили эту проблему в PSP Quake 1, благо исходники доступны для анализа.

Что же мы видим из анализа исходников? А по сути вот что:

  //получаем матрицу проецирования
  sceGumMatrixMode(GU_PROJECTION);
  ScePspFMatrix4 projection_matrix;
  sceGumStoreMatrix(&projection_matrix);
  //получаем матрицу видового преобразования
  sceGumMatrixMode(GU_VIEW);
  ScePspFMatrix4 view_matrix;
  sceGumStoreMatrix(&view_matrix);
  //получаем матрицу моделирования
  sceGumMatrixMode(GU_MODEL);
  ScePspFMatrix4 model_matrix;
  sceGumStoreMatrix(&model_matrix);
  sceGuFinish();

  //вычисляем общую матрицу view-projection
  ScePspFMatrix4 projection_view_matrix;
  MultiplyScePspFMatrix4(view_matrix,projection_matrix,projection_view_matrix);
  //вычисляем общую матрицу view-projection-model
  ScePspFMatrix4 projection_view_model_matrix;
  MultiplyScePspFMatrix4(model_matrix,projection_view_matrix,projection_view_model_matrix);
  //вычисляем матрицу view-model
  ScePspFMatrix4 view_model_matrix;
  MultiplyScePspFMatrix4(model_matrix,view_matrix,view_model_matrix);
  //вычисляем четыре плоскости отсечения по проекции (верх, низ, лево, право)
  ScePspFVector4 frustum[4];//четверка чисел описывает плоскость: ax+by+cz+d=0
  //левая
  frustum[0].x=projection_view_model_matrix.x.w+projection_view_model_matrix.x.x;
  frustum[0].y=projection_view_model_matrix.y.w+projection_view_model_matrix.y.x;
  frustum[0].z=projection_view_model_matrix.z.w+projection_view_model_matrix.z.x;
  frustum[0].w=projection_view_model_matrix.w.w+projection_view_model_matrix.w.x;
  NormaliseScePspFVector4(frustum[0]);
  //правая
  frustum[1].x=projection_view_model_matrix.x.w-projection_view_model_matrix.x.x;
  frustum[1].y=projection_view_model_matrix.y.w-projection_view_model_matrix.y.x;
  frustum[1].z=projection_view_model_matrix.z.w-projection_view_model_matrix.z.x;
  frustum[1].w=projection_view_model_matrix.w.w-projection_view_model_matrix.w.x;
  NormaliseScePspFVector4(frustum[1]);
  //верхняя
  frustum[2].x=projection_view_model_matrix.x.w-projection_view_model_matrix.x.y;
  frustum[2].y=projection_view_model_matrix.y.w-projection_view_model_matrix.y.y;
  frustum[2].z=projection_view_model_matrix.z.w-projection_view_model_matrix.z.y;
  frustum[2].w=projection_view_model_matrix.w.w-projection_view_model_matrix.w.y;
  NormaliseScePspFVector4(frustum[2]);
  //нижняя
  frustum[3].x=projection_view_model_matrix.x.w+projection_view_model_matrix.x.y;
  frustum[3].y=projection_view_model_matrix.y.w+projection_view_model_matrix.y.y;
  frustum[3].z=projection_view_model_matrix.z.w+projection_view_model_matrix.z.y;
  frustum[3].w=projection_view_model_matrix.w.w+projection_view_model_matrix.w.y;
  NormaliseScePspFVector4(frustum[3]);

То есть, в Quake 1 перед выводом просто переносят все точки внутрь объёма, ограничивающего взгляд, либо выбрасывают их вовсе (если вся фигура не видна). Как же это сделать? Нужно просто считать три матрицы — GU_PROJECTION, GU_MODEL, GU_VIEW. Перемножить их и получить итоговую матрицу преобразования координат. Из этой матрицы можно вытащить все нужные ограничивающие вид плоскости (4 компоненты полученного вектора задают плоскость с уравнением ax+by+cz+w=0). (a,b,c) — вектор нормали, а w=a*x0+b*y0+c*z0 — характеризует некую точку (x0,y0,z0) плоскости. Сами координаты точки нам не нужны — достаточно знать w.

Отсечение выполняется следующим образом (для четырёх вышеуказанных плоскостей по-очереди в цикле):

  //выполняем отсечение
  vector<SGuNVCTPoint> vector_clip_point;
  for(long n=0;n<4;n++)
  {
   float nx=frustum[n].x;
   float ny=frustum[n].y;
   float nz=frustum[n].z;
   float w=frustum[n].w;

   Clip(vector_point,vector_clip_point,nx,ny,nz,w);
   vector_point=vector_clip_point;
  }

Но для такого фокуса нам потребуются следующие функции (списанные из Quake 1):

//----------------------------------------------------------------------------------------------------
//получить точку пересечения прямой и плоскости
//----------------------------------------------------------------------------------------------------
void CMain::GetIntersectionPlaneAndLine(const SGuNVCTPoint& A,const SGuNVCTPoint& B,SGuNVCTPoint& new_point,float nx,float ny,float nz,float w)
{
 new_point=A;
 float ax=A.sGuVertex.X;
 float ay=A.sGuVertex.Y;
 float az=A.sGuVertex.Z;
 float au=A.sGuTexture.U;
 float av=A.sGuTexture.V;

 float bx=B.sGuVertex.X;
 float by=B.sGuVertex.Y;
 float bz=B.sGuVertex.Z;
 float bu=B.sGuTexture.U;
 float bv=B.sGuTexture.V;

 float dx=bx-ax;
 float dy=by-ay;
 float dz=bz-az;
 float du=bu-au;
 float dv=bv-av;

 float top=(nx*ax)+(ny*ay)+(nz*az)+w;
 float bottom=(nx*dx)+(ny*dy)+(nz*dz);
 float time=-top/bottom;

 float vx=ax+time*dx;
 float vy=ay+time*dy;
 float vz=az+time*dz;
 float vu=au+time*du;
 float vv=av+time*dv;
 //добавляем новую точку
 SetVertexCoord(new_point.sGuVertex,vx,vy,vz);
 SetTextureCoord(new_point.sGuTexture,vu,vv);
}
//----------------------------------------------------------------------------------------------------
//выполнить коррекцию координат
//----------------------------------------------------------------------------------------------------
void CMain::Clip(const vector<SGuNVCTPoint>& vector_point_input,vector<SGuNVCTPoint>& vector_point_output,float nx,float ny,float nz,float w)
{
 vector_point_output.clear();
 long point=vector_point_input.size();
 for(long n=0;n<point;n++)
 {
  long next_p=n+1;
  if (next_p>=point) next_p-=point;

  const SGuNVCTPoint *sGuNVCTPoint_Current_Ptr=&(vector_point_input[n]);
  float current_vx=sGuNVCTPoint_Current_Ptr->sGuVertex.X;
  float current_vy=sGuNVCTPoint_Current_Ptr->sGuVertex.Y;
  float current_vz=sGuNVCTPoint_Current_Ptr->sGuVertex.Z;
  //определяем положение относительно плоскости отсечения
  float current_ret=current_vx*nx+current_vy*ny+current_vz*nz+w;

  const SGuNVCTPoint *sGuNVCTPoint_Next_Ptr=&(vector_point_input[next_p]);
  float next_vx=sGuNVCTPoint_Next_Ptr->sGuVertex.X;
  float next_vy=sGuNVCTPoint_Next_Ptr->sGuVertex.Y;
  float next_vz=sGuNVCTPoint_Next_Ptr->sGuVertex.Z;
  //определяем положение относительно плоскости отсечения
  float next_ret=next_vx*nx+next_vy*ny+next_vz*nz+w;

  if (current_ret>0)//текущая точка видима
  {
   if (next_ret>0)//следующая точка видима
   {
    vector_point_output.push_back(*sGuNVCTPoint_Next_Ptr);
   }
   else
   {
    //добавляем новую точку пересечения
    SGuNVCTPoint sGuNVCTPoint_New;
    GetIntersectionPlaneAndLine(*sGuNVCTPoint_Current_Ptr,*sGuNVCTPoint_Next_Ptr,sGuNVCTPoint_New,nx,ny,nz,w);
    vector_point_output.push_back(sGuNVCTPoint_New);
   }
  }
  else//текущая точка не видна
  {
   if (next_ret>0)//следующая точка видна
   {
    //добавляем новую точку пересечения
    SGuNVCTPoint sGuNVCTPoint_New;
    GetIntersectionPlaneAndLine(*sGuNVCTPoint_Current_Ptr,*sGuNVCTPoint_Next_Ptr,sGuNVCTPoint_New,nx,ny,nz,w);
    vector_point_output.push_back(sGuNVCTPoint_New);
    //добавляем сдудующую точку
    vector_point_output.push_back(*sGuNVCTPoint_Next_Ptr);
   }
  }
 }
}

И вот только после выполнения такого отсечения у вас, наконец-таки, корректно заработает вывод трёхмерной графики на PSP с помощью GU. Можете создавать игру! :)

Отображение трёхмерной графики на PSP - 1

Кстати, можно также использовать для скалярного произведения векторов векторный процессор PSP. Например, вот функция, определяющая требуется ли вообще отсечение (выдранная по кусочкам из того же Quake 1 для PSP):

//выполняем отсечение
 vector<SGuNVCTPoint> vector_clip_point;
 //используем векторный процессор PSP
 __asm__ volatile
 (
  "ulv.q	C700, %0n"	//загружаем вектор в регистр
  "ulv.q	C710, %1n"	//загружаем вектор в регистр
  "ulv.q	C720, %2n"	//загружаем вектор в регистр
  "ulv.q	C730, %3n"	//загружаем вектор в регистр
  :: "m"(FrustumPlane[0]),"m"(FrustumPlane[1]),"m"(FrustumPlane[2]),"m"(FrustumPlane[3])
 );
 //проверим необходимость отсечения
 long vertex=vector_point.size();
 bool clipping=false;
 for(long n=0;n<vertex;n++)
 {
  ScePspFVector4 current_vertex;
  current_vertex.x=vector_point[n].sGuVertex.X;
  current_vertex.y=vector_point[n].sGuVertex.Y;
  current_vertex.z=vector_point[n].sGuVertex.Z;
  current_vertex.w=1;
  float ret1,ret2,ret3,ret4;
  __asm__ volatile
  (
   "ulv.q	C610, %4n"			// загружаем вектор вершины в регистр
   "vone.s	S613n"				// ставим единицу в четвёртой компоненте вектора
   "vdot.q	S620, C700, C610n"	// s620 = вычисляем скалярное произведение
   "vdot.q	S621, C710, C610n"	// s621 = вычисляем скалярное произведение
   "vdot.q	S622, C720, C610n"	// s622 = вычисляем скалярное произведение
   "vdot.q	S623, C730, C610n"	// s623 = вычисляем скалярное произведение
   "mfv	%0, S620n"			// out1 = s620
   "mfv	%1, S621n"			// out2 = s621
   "mfv	%2, S622n"			// out3 = s622
   "mfv	%3, S623n"			// out4 = s623
   : "=r"(ret1), "=r"(ret2), "=r"(ret3), "=r"(ret4) : "m"(current_vertex)
  );
  if (ret1<0 || ret2<0 || ret3<0 || ret4<0)//требуется отсечение
  {
   clipping=true;
   break;
  }
 }

Тут всё просто — поместили вектора плоскостей и координаты точки в регистры и попросили VFPU выполнить скалярное произведение.

Ссылка на простейшее приложение, выводящее текстуру

Ссылка на движок для PSP с использованием GU

P.S. Я знаю, тут бывают профессионалы по программированию для PSP. Может быть они расскажут, почему GU у PSP так устроен и как правильно работать с ним.

Автор:

Отображение трёхмерной графики на PSP / Geektimes

font-face{font-family:'Fira Sans';font-style:normal;font-weight:500;src:url(/fonts/0/FiraSans/firaSans-medium.eot);src:local("Fira Sans Medium"),local("FiraSans-Medium"),url(/fonts/0/FiraSans/firaSans-medium.eot?#iefix) format("embedded-opentype"),url(/fonts/0/FiraSans/firaSans-medium.woff2) format("woff2"),url(/fonts/0/FiraSans/firaSans-medium.woff) format("woff"),url(/fonts/0/FiraSans/firaSans-medium.ttf) format("truetype")}

/* Font Face Observer v2.0.13 - © Bram Stein. License: BSD-3-Clause */(function(){'use strict';var f,g=[];function l(a){g.push(a);1==g.length&&f()}function m(){for(;g.length;)g[0](),g.shift()}f=function(){setTimeout(m)};function n(a){this.a=p;this.b=void 0;this.f=[];var b=this;try{a(function(a){q(b,a)},function(a){r(b,a)})}catch(c){r(b,c)}}var p=2;function t(a){return new n(function(b,c){c(a)})}function u(a){return new n(function(b){b(a)})}function q(a,b){if(a.a==p){if(b==a)throw new TypeError;var c=!1;try{var d=b&&b.then;if(null!=b&&"object"==typeof b&&"function"==typeof d){d.call(b,function(b){c||q(a,b);c=!0},function(b){c||r(a,b);c=!0});return}}catch(e){c||r(a,e);return}a.a=0;a.b=b;v(a)}}
function r(a,b){if(a.a==p){if(b==a)throw new TypeError;a.a=1;a.b=b;v(a)}}function v(a){l(function(){if(a.a!=p)for(;a.f.length;){var b=a.f.shift(),c=b[0],d=b[1],e=b[2],b=b[3];try{0==a.a?"function"==typeof c?e(c.call(void 0,a.b)):e(a.b):1==a.a&&("function"==typeof d?e(d.call(void 0,a.b)):b(a.b))}catch(h){b(h)}}})}n.prototype.g=function(a){return this.c(void 0,a)};n.prototype.c=function(a,b){var c=this;return new n(function(d,e){c.f.push([a,b,d,e]);v(c)})};
function w(a){return new n(function(b,c){function d(c){return function(d){h[c]=d;e+=1;e==a.length&&b(h)}}var e=0,h=[];0==a.length&&b(h);for(var k=0;k<a.length;k+=1)u(a[k]).c(d(k),c)})}function x(a){return new n(function(b,c){for(var d=0;dparseInt(a[1],10)}else C=!1;return C}function J(){null===F&&(F=!!document.fonts);return F}
function K(){if(null===E){var a=document.createElement("div");try{a.style.font="condensed 100px sans-serif"}catch(b){}E=""!==a.style.font}return E}function L(a,b){return[a.style,a.weight,K()?a.stretch:"","100px",b].join(" ")}
A.prototype.load=function(a,b){var c=this,k=a||"BESbswy",q=0,D=b||3E3,H=(new Date).getTime();return new Promise(function(a,b){if(J()&&!G()){var M=new Promise(function(a,b){function e(){(new Date).getTime()-H>=D?b():document.fonts.load(L(c,'"'+c.family+'"'),k).then(function(c){1parseInt(b[1],10)||536===parseInt(b[1],10)&&11>=parseInt(b[2],10))),b=B&&(f==v&&g==v&&h==v||f==w&&g==w&&h==w||f==x&&g==x&&h==x)),b=!b;b&&(d.parentNode&&d.parentNode.removeChild(d),clearTimeout(q),a(c))}function I(){if((new Date).getTime()-H>=D)d.parentNode&&d.parentNode.removeChild(d),b(c);else{var a=document.hidden;if(!0===a||void 0===a)f=e.a.offsetWidth,
g=n.a.offsetWidth,h=p.a.offsetWidth,u();q=setTimeout(I,50)}}var e=new r(k),n=new r(k),p=new r(k),f=-1,g=-1,h=-1,v=-1,w=-1,x=-1,d=document.createElement("div");d.dir="ltr";t(e,L(c,"sans-serif"));t(n,L(c,"serif"));t(p,L(c,"monospace"));d.appendChild(e.a);d.appendChild(n.a);d.appendChild(p.a);document.body.appendChild(d);v=e.a.offsetWidth;w=n.a.offsetWidth;x=p.a.offsetWidth;I();z(e,function(a){f=a;u()});t(e,L(c,'"'+c.family+'",sans-serif'));z(n,function(a){g=a;u()});t(n,L(c,'"'+c.family+'",serif'));
z(p,function(a){h=a;u()});t(p,L(c,'"'+c.family+'",monospace'))})})};"object"===typeof module?module.exports=A:(window.FontFaceObserver=A,window.FontFaceObserver.prototype.load=A.prototype.load);}());

(function( w ){
if( w.document.documentElement.className.indexOf( "fonts-loaded" ) > -1 ){ return; }

var html = document.documentElement;
var FS500 = new w.FontFaceObserver("Fira Sans", { weight: 500 });

FS500.load().then(function() {
html.classList.add('fonts-loaded');
sessionStorage.fontsLoaded = true;
console.log('FS500-loaded');
}).catch(function () {
sessionStorage.fontsLoaded = false;
console.log('FS500-unloaded');
});

if (sessionStorage.fontsLoaded) {
html.classList.add('fonts-loaded');
}
}(this));

var N = 5; var ar_duo1 = Math.floor(Math.random()*N+1);

if (typeof adbl == 'undefined'){ var adbl = 'yes';}

var user_type = "guest";

var page_type = "publish_ugc";

(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');

ga('create', 'UA-726094-21', 'auto');
ga('create', 'UA-726094-24', 'auto', {'name': 'HGM'});

ga('require', 'displayfeatures');
ga('set', 'dimension1', user_type); // user type - guest/readonly/habrauser
ga('set', 'dimension2', adbl);

ga('set', 'dimension3', page_type);

(function() {
var win = window;
var removeUtms = function(){
var location = win.location;
if (location.search.indexOf('utm_') != -1 && history.replaceState) {
history.replaceState({}, '', window.location.toString().replace(/(&|?)utm([_a-z0-9=-]+)/g, ""));
}
};
ga('send', 'pageview', { 'hitCallback': removeUtms });
})();

ga('HGM.set', 'dimension1', user_type);
ga('HGM.set', 'dimension2', "geektimes");
ga('HGM.send', 'pageview');

var adcm_config ={
id:1034,
platformId: 34,

tags: ['hub_antikvariat', 'hub_programming', 'g_computer_hardware', 'g_programming'],
init: function () {
window.adcm.call();
}
};

Отображение трёхмерной графики на PSP

Пару месяцев назад я вновь достал из ящика запылившуюся PSP и решил портировать туда мой ранее уже показанный движок. С программной отрисовкой проблем не возникло – всё и так работает. А вот с использованием GU всё оказалось не так просто. В данной статье я покажу на примере, как можно написать для PSP простое трёхмерное приложение, использующее GU.

Заранее предупреждаю, что руководств по программированию для PSP довольно мало, и поэтому какие-то мои выводы могут оказаться неверными. Но, к делу.

Главная функция программы для PSP, если кто не знает, оформляется примерно вот так:

#include <pspkernel.h>
#include <pspdebug.h>
#include <pspdisplay.h>

//----------------------------------------------------------------------------------------
PSP_MODULE_INFO("GUTexture", 0, 1, 1);
PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER|THREAD_ATTR_VFPU);

void dump_threadstatus(void);

bool done=false;

int exit_callback(int arg1,int arg2,void *common)
{
 done=true;
 return(0);
}

int CallbackThread(SceSize args, void *argp)
{
 int cbid;
 cbid=sceKernelCreateCallback("Exit Callback",exit_callback,NULL);
 sceKernelRegisterExitCallback(cbid);
 sceKernelSleepThreadCB();
 return(0);
}
int SetupCallbacks(void)
{
 int thid = 0;
 thid=sceKernelCreateThread("update_thread",CallbackThread,0x11,0xFA0,0,0);
 if(thid>=0) sceKernelStartThread(thid, 0, 0);
 return(thid);
}
//----------------------------------------------------------------------------------------
//начинаем программу
//----------------------------------------------------------------------------------------

int main(int argc, char  **argv)
{
 pspDebugScreenInit();
 //устанавливаем обработчики
 SetupCallbacks();
 //выполняем программу 
 ……….
 //выходим из программы
 sceKernelExitGame();
 return(0);
}

Инициализация GU выполняется следующим образом:

Сначала мы запрашиваем указатели на три буфера – экранный, внеэкранный и буфер глубины (Z-буфер). Буферы выравниваются по 512 пикселей в строке (хотя у PSP строка 480 пикселей). Также требуется учесть формат цвета пикселя. В данном примере использован формат GU_PSM_8888 — по 8 бит на R,G,B и Alpha-компоненты цвета пикселя. Для Z-буффера использован формат GU_PSM_4444 просто потому что это 16 бит — Z-буфер у PSP 16 битный.

//размеры экрана
#define SCREEN_WIDTH 480
#define SCREEN_HEIGHT 272
#define SCREEN_LINE_WIDTH 512

void* fbp0=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_8888);
void* fbp1=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_8888);
void* zbp=getStaticVramBuffer(SCREEN_LINE_WIDTH, SCREEN_HEIGHT,GU_PSM_4444);

Функция запроса указателей на буферы определяется как

#include <pspge.h>
#include <pspgu.h>

static unsigned int staticOffset=0;

static unsigned int getMemorySize(unsigned int width,unsigned int height,unsigned int psm)
{
 switch (psm)
 {
  case GU_PSM_T4: return((width*height)>>1);
  case GU_PSM_T8: return(width*height);
  case GU_PSM_5650:
  case GU_PSM_5551:
  case GU_PSM_4444:
  case GU_PSM_T16: return(2*width*height);
  case GU_PSM_8888:
  case GU_PSM_T32: return(4*width*height);
  default: return(0);
 }
}

void* getStaticVramBuffer(unsigned int width,unsigned int height,unsigned int psm)
{
 unsigned int memSize=getMemorySize(width,height,psm);
 void* result=(void*)staticOffset;
 staticOffset+=memSize;
 return(result);
}

void* getStaticVramTexture(unsigned int width,unsigned int height,unsigned int psm)
{
 void* result=getStaticVramBuffer(width,height,psm);
 return((void*)(((unsigned int)result) + ((unsigned int)sceGeEdramGetAddr())));
}

Это не мои функции – я их взял из какой-то программы давным-давно и лишь слегка изменил. Память распределяется в области видеопамяти. Текстуры также следует по возможности размещать там же, запрашивая указатель через getStaticVramTexture, иначе быстродействие резко упадёт. Разумеется, никакой динамической памяти при таких запросах не выделяется, а просто распределяется часть заданного адресного пространства PSP под экран и текстуры. Насколько я помню, видеопамяти у PSP всего 2 мегабайта — это очень мало для хранения множества текстур.

Программирование GU у PSP похоже на программирование для OpenGL с одним отличием — выполнение команд требует их размещения в дисплейном списке, причём память для этого списка должна быть заранее выделена и при этом выровнена:
static unsigned char __attribute__((aligned(16))) DisplayList[262144];
Команды, относящиеся к преобразованию координат не требуют дисплейного списка и могут выполняться в любом месте программы.

Инициализировать GU можно, например, так:

//размер сторон виртуального экрана PSP
#define VIRTUAL_SCREEN_SIZE 2048
//соотношение сторон экрана
#define SCREEN_ASPECT 16.0f/9.0f
//передняя плоскость отсечения
#define NEAR_PLANE_Z 5.0f
//задняя плоскость отсечения
#define FAR_PLANE_Z 4096.0f
//угол зрения
#define EYE_ANGLE 60.0f

//инициализируем графику GU
 sceGuInit();
 //создаём и запускаем на выполнение новый контекст дисплея - он должен выполниться сразу, т.к. GU_DIRECT
 sceGuStart(GU_DIRECT,DisplayList);
 //устанавливаем параметры буфера рисования- формат пикселя, указатель на область видеопамяти, длину строки (выровненную, а не физическую)
 sceGuDrawBuffer(GU_PSM_8888,fbp0,SCREEN_LINE_WIDTH);
 //устанавливаем параметры буфера экрана - размер экрана, указатель на видеопамять, длину строки
 sceGuDispBuffer(SCREEN_WIDTH,SCREEN_HEIGHT,fbp1,SCREEN_LINE_WIDTH);
 //устанавливаем параметры буфера глубины- указатель на начало буфера глубины в видеопамяти и длину строки
 sceGuDepthBuffer(zbp,SCREEN_LINE_WIDTH);
 //устанавливаем смещение экрана в общем пространстве 4096x4096 (в PSP такой размер виртуального экрана)
 sceGuOffset(VIRTUAL_SCREEN_SIZE-(SCREEN_WIDTH/2),VIRTUAL_SCREEN_SIZE-(SCREEN_HEIGHT/2));//ставим по центру
 //настраиваем видовой порт - порт просмотра- координаты центра и размеры сторон
 sceGuViewport(VIRTUAL_SCREEN_SIZE,VIRTUAL_SCREEN_SIZE,SCREEN_WIDTH,SCREEN_HEIGHT);
 //устанавливаем диапазон значений для буфера глубины - передняя и задняя плоскости отсечения (буфер инвертирован и значения от 0 до 65535 !)
 sceGuDepthRange(65535,0);
 //включаем обрезание области показа по размерам видового порта
 sceGuScissor(0,0,SCREEN_WIDTH,SCREEN_HEIGHT);
 sceGuEnable(GU_SCISSOR_TEST);
 sceGuEnable(GU_CLIP_PLANES);
  //настроим матрицу проецирования
 sceGumMatrixMode(GU_PROJECTION);
 sceGumLoadIdentity();
 sceGumPerspective(EYE_ANGLE,SCREEN_ASPECT,NEAR_PLANE_Z,FAR_PLANE_Z);
 //включим режим гладкой интерполяции цвета граней
 sceGuShadeModel(GU_SMOOTH);
 //включим тест глубины
 sceGuDepthFunc(GU_GEQUAL);
 sceGuEnable(GU_DEPTH_TEST);
 sceGuDepthMask(GU_FALSE);
 //отключим режим отсечения граней, повёрнутых обратной стороной к наблюдателю
 sceGuFrontFace(GU_CCW);
 sceGuDisable(GU_CULL_FACE);
 //настраиваем прозрачность
 sceGuDisable(GU_BLEND);
 sceGuBlendFunc(GU_ADD,GU_SRC_ALPHA,GU_ONE_MINUS_SRC_ALPHA,0,0);
 //выполняем созданный список
 sceGuFinish();
 sceGuSync(GU_SYNC_WAIT,GU_SYNC_FINISH);
 sceGuDisplay(GU_TRUE); 

После завершения работы с GU следует вызвать sceGuTerm().

После загрузки текстуры размера (WidthImage; HeightImage) любым удобным способом (указатель Data на данные текстуры – и лучше его получать в области видеопамяти), мы можем её вывести на экран.

  //рисуем сцену
  sceGuStart(GU_DIRECT,DisplayList);
  //очистим экран и буфер глубины
  sceGuClearColor(0);
  sceGuClearDepth(0);
  sceGuClear(GU_COLOR_BUFFER_BIT|GU_DEPTH_BUFFER_BIT);

  //настроим матрицу проецирования
  sceGumMatrixMode(GU_PROJECTION);
  sceGumLoadIdentity();
  sceGumPerspective(EYE_ANGLE,SCREEN_ASPECT,NEAR_PLANE_Z,FAR_PLANE_Z);
  sceGumUpdateMatrix();.

  //инициализируем матрицы
  sceGumMatrixMode(GU_TEXTURE);
  sceGumLoadIdentity();
  sceGumMatrixMode(GU_VIEW);
  sceGumLoadIdentity();
  sceGumMatrixMode(GU_MODEL);
  sceGumLoadIdentity();

  //выводим прямоугольник с текстурой
  sceGuColor(0xffffffff);//цвет окраски
  sceGuEnable(GU_TEXTURE_2D);
  sceGuTexMode(GU_PSM_8888,0,0,0);
  sceGuTexImage(0,WidthImage,HeightImage,WidthImage,Data);
  sceGuTexFunc(GU_TFX_MODULATE,GU_TCC_RGBA);
  sceGuTexFilter(GU_NEAREST,GU_NEAREST);
  sceGuTexWrap(GU_REPEAT,GU_REPEAT);
  sceGuTexScale(1,1);
  sceGuTexOffset(0,0);
 
  //выводим полигон по точкам из массива
  …
    
  sceGuDisable(GU_TEXTURE_2D);
  //запускаем список на выполнение
  sceGuFinish();
  sceGuSync(GU_SYNC_WAIT,GU_SYNC_FINISH);
  //делаем видимым буфер, в котором мы рисовали
  sceDisplayWaitVblankStart();
  sceGuSwapBuffers();

Как вывести полигон? Для рисования геометрии GU у PSP просит поместить все точки в массив, указатель на который нужно предварительно получить командой sceGuGetMemory, передав ей размер запрашиваемого блока памяти в байтах. Дальше по этому указателю вы должны записать массив точек и попросить PSP их вывести, например, командой sceGumDrawArray с нужными параметрами. Но каков формат этих точек? У PSP данные точек располагаются в определённом порядке и размер массива, описывающего одну точку должен быть кратен 32 байтам: Вес вершины, текстурные координаты, цвет точки, нормаль к точке, координата точки. Именно в таком порядке. Чтобы не заморачиваться с форматом, я определил набор структур и функций для работы с ними:

//#pragma pack(1)
//[for vertices(1-8)] [weights (0-8)] [texture uv] [color] [normal] [vertex] [/for]

#pragma pack(1)

//координата точки
struct SGuVertex
{
 float X;
 float Y;
 float Z;
};
//нормаль к точке
struct SGuNormal
{
 float Nx;
 float Ny;
 float Nz;
};
//текстурные координаты
struct SGuTexture
{
 float U;
 float V;
};
//цвет точки
struct SGuColor
{
 unsigned long Color;
};
#pragma pack()

#pragma pack(32)
//точка с текстурой, цветом, нормалью, координатами
struct SGuNVCTPoint
{
 SGuTexture sGuTexture;
 SGuColor sGuColor;
 SGuNormal sGuNormal;
 SGuVertex sGuVertex;
};
#pragma pack()

  void SetVertexCoord(SGuVertex &sGuVertex,float x,float y,float z);//задать координаты вершины
  void SetNormalCoord(SGuNormal &sGuNormal,float nx,float ny,float nz);//задать координаты нормали
  void SetTextureCoord(SGuTexture &sGuTexture,float u,float v);//задать координаты текстуры
  void SetColorValue(SGuColor &sGuColor,unsigned long color);//задать цвет

//----------------------------------------------------------------------------------------------------
//задать координаты вершины
//----------------------------------------------------------------------------------------------------
void CMain::SetVertexCoord(SGuVertex &sGuVertex,float x,float y,float z)
{
 sGuVertex.X=x;
 sGuVertex.Y=y;
 sGuVertex.Z=z;
}
//----------------------------------------------------------------------------------------------------
//задать координаты нормали
//----------------------------------------------------------------------------------------------------
void CMain::SetNormalCoord(SGuNormal &sGuNormal,float nx,float ny,float nz)
{
 sGuNormal.Nx=nx;
 sGuNormal.Ny=ny;
 sGuNormal.Nz=nz;
}
//----------------------------------------------------------------------------------------------------
//задать координаты текстуры
//----------------------------------------------------------------------------------------------------
void CMain::SetTextureCoord(SGuTexture &sGuTexture,float u,float v)
{
 sGuTexture.U=u;
 sGuTexture.V=v;
}
//----------------------------------------------------------------------------------------------------
//задать цвет
//----------------------------------------------------------------------------------------------------
void CMain::SetColorValue(SGuColor &sGuColor,unsigned long color)
{
 sGuColor.Color=color;
}

Тогда задать геометрию (в данном случае — квадрат) можно, например, так:

  //задаём геометрию
  SGuNVCTPoint sGuNVCTPoint;
  vector<SGuNVCTPoint> vector_point;

  SetVertexCoord(sGuNVCTPoint.sGuVertex,-100,100,0);
  SetTextureCoord(sGuNVCTPoint.sGuTexture,0,0);
  SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1);
  SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF);
  vector_point.push_back(sGuNVCTPoint);

  SetVertexCoord(sGuNVCTPoint.sGuVertex,100,100,0);
  SetTextureCoord(sGuNVCTPoint.sGuTexture,1,0);
  SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1);
  SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF);
  vector_point.push_back(sGuNVCTPoint);

  SetVertexCoord(sGuNVCTPoint.sGuVertex,100,-100,0);
  SetTextureCoord(sGuNVCTPoint.sGuTexture,1,1);
  SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1);
  SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF);
  vector_point.push_back(sGuNVCTPoint);

  SetVertexCoord(sGuNVCTPoint.sGuVertex,-100,-100,0);
  SetTextureCoord(sGuNVCTPoint.sGuTexture,0,1);
  SetNormalCoord(sGuNVCTPoint.sGuNormal,0,0,1);
  SetColorValue(sGuNVCTPoint.sGuColor,0xFFFFFFFF);
  vector_point.push_back(sGuNVCTPoint);

А вывести его, например, так:

  size_t vertex_amount=vector_point.size();
  SGuNVCTPoint *sGuNVCTPoint_Ptr=(SGuNVCTPoint*)sceGuGetMemory(vertex_amount*sizeof(SGuNVCTPoint));
  if (sGuNVCTPoint_Ptr!=NULL)
  {
   for(size_t n=0;n<vertex_amount;n++) sGuNVCTPoint_Ptr[n]=vector_point[n];
   sceGumDrawArray(GU_TRIANGLE_FAN,GU_COLOR_8888|GU_VERTEX_32BITF|GU_TRANSFORM_3D|GU_NORMAL_32BITF|GU_TEXTURE_32BITF,vertex_amount,0,sGuNVCTPoint_Ptr);
  }

Для вывода я указал функции sceGumDrawArray, что именно я рисую и каков формат точки ( GU_COLOR_8888|GU_VERTEX_32BITF|GU_TRANSFORM_3D|GU_NORMAL_32BITF|GU_TEXTURE_32BITF — точка состоит из цвета, координат, нормали, текстурных координат и требует перемножения координат на соответствующие матрицы перед рисованием). Рисование возможно только треугольниками. Но это ещё не всё…

Вроде бы всё работает, но работает только, если все точки находятся перед глазами и видимы. Стоит хотя бы одной точки уйти в какую-то туманную даль, как GU отказывается рисовать весь многоугольник. Как я понимаю, GU у PSP требует, чтоб относительно четырёх плоскостей отсечения (левая, правая, верхняя и нижняя (а передняя получится автоматически)) точка лежала внутри этого объёма, иначе GU не согласен её выводить. Проблема. Но в играх-то 3D-графика присутствует и таких артефактов не наблюдается! Давайте посмотрим, как решили эту проблему в PSP Quake 1, благо исходники доступны для анализа.

Что же мы видим из анализа исходников? А по сути вот что:

  //получаем матрицу проецирования
  sceGumMatrixMode(GU_PROJECTION);
  ScePspFMatrix4 projection_matrix;
  sceGumStoreMatrix(&projection_matrix);
  //получаем матрицу видового преобразования
  sceGumMatrixMode(GU_VIEW);
  ScePspFMatrix4 view_matrix;
  sceGumStoreMatrix(&view_matrix);
  //получаем матрицу моделирования
  sceGumMatrixMode(GU_MODEL);
  ScePspFMatrix4 model_matrix;
  sceGumStoreMatrix(&model_matrix);
  sceGuFinish();

  //вычисляем общую матрицу view-projection
  ScePspFMatrix4 projection_view_matrix;
  MultiplyScePspFMatrix4(view_matrix,projection_matrix,projection_view_matrix);
  //вычисляем общую матрицу view-projection-model
  ScePspFMatrix4 projection_view_model_matrix;
  MultiplyScePspFMatrix4(model_matrix,projection_view_matrix,projection_view_model_matrix);
  //вычисляем матрицу view-model
  ScePspFMatrix4 view_model_matrix;
  MultiplyScePspFMatrix4(model_matrix,view_matrix,view_model_matrix);
  //вычисляем четыре плоскости отсечения по проекции (верх, низ, лево, право)
  ScePspFVector4 frustum[4];//четверка чисел описывает плоскость: ax+by+cz+d=0
  //левая
  frustum[0].x=projection_view_model_matrix.x.w+projection_view_model_matrix.x.x;
  frustum[0].y=projection_view_model_matrix.y.w+projection_view_model_matrix.y.x;
  frustum[0].z=projection_view_model_matrix.z.w+projection_view_model_matrix.z.x;
  frustum[0].w=projection_view_model_matrix.w.w+projection_view_model_matrix.w.x;
  NormaliseScePspFVector4(frustum[0]);
  //правая
  frustum[1].x=projection_view_model_matrix.x.w-projection_view_model_matrix.x.x;
  frustum[1].y=projection_view_model_matrix.y.w-projection_view_model_matrix.y.x;
  frustum[1].z=projection_view_model_matrix.z.w-projection_view_model_matrix.z.x;
  frustum[1].w=projection_view_model_matrix.w.w-projection_view_model_matrix.w.x;
  NormaliseScePspFVector4(frustum[1]);
  //верхняя
  frustum[2].x=projection_view_model_matrix.x.w-projection_view_model_matrix.x.y;
  frustum[2].y=projection_view_model_matrix.y.w-projection_view_model_matrix.y.y;
  frustum[2].z=projection_view_model_matrix.z.w-projection_view_model_matrix.z.y;
  frustum[2].w=projection_view_model_matrix.w.w-projection_view_model_matrix.w.y;
  NormaliseScePspFVector4(frustum[2]);
  //нижняя
  frustum[3].x=projection_view_model_matrix.x.w+projection_view_model_matrix.x.y;
  frustum[3].y=projection_view_model_matrix.y.w+projection_view_model_matrix.y.y;
  frustum[3].z=projection_view_model_matrix.z.w+projection_view_model_matrix.z.y;
  frustum[3].w=projection_view_model_matrix.w.w+projection_view_model_matrix.w.y;
  NormaliseScePspFVector4(frustum[3]);

То есть, в Quake 1 перед выводом просто переносят все точки внутрь объёма, ограничивающего взгляд, либо выбрасывают их вовсе (если вся фигура не видна). Как же это сделать? Нужно просто считать три матрицы — GU_PROJECTION, GU_MODEL, GU_VIEW. Перемножить их и получить итоговую матрицу преобразования координат. Из этой матрицы можно вытащить все нужные ограничивающие вид плоскости (4 компоненты полученного вектора задают плоскость с уравнением ax+by+cz+w=0). (a,b,c) — вектор нормали, а w=a*x0+b*y0+c*z0 — характеризует некую точку (x0,y0,z0) плоскости. Сами координаты точки нам не нужны — достаточно знать w.

Отсечение выполняется следующим образом (для четырёх вышеуказанных плоскостей по-очереди в цикле):

  //выполняем отсечение
  vector<SGuNVCTPoint> vector_clip_point;
  for(long n=0;n<4;n++)
  {
   float nx=frustum[n].x;
   float ny=frustum[n].y;
   float nz=frustum[n].z;
   float w=frustum[n].w;

   Clip(vector_point,vector_clip_point,nx,ny,nz,w);
   vector_point=vector_clip_point;
  }

Но для такого фокуса нам потребуются следующие функции (списанные из Quake 1):

//----------------------------------------------------------------------------------------------------
//получить точку пересечения прямой и плоскости
//----------------------------------------------------------------------------------------------------
void CMain::GetIntersectionPlaneAndLine(const SGuNVCTPoint& A,const SGuNVCTPoint& B,SGuNVCTPoint& new_point,float nx,float ny,float nz,float w)
{
 new_point=A;
 float ax=A.sGuVertex.X;
 float ay=A.sGuVertex.Y;
 float az=A.sGuVertex.Z;
 float au=A.sGuTexture.U;
 float av=A.sGuTexture.V;

 float bx=B.sGuVertex.X;
 float by=B.sGuVertex.Y;
 float bz=B.sGuVertex.Z;
 float bu=B.sGuTexture.U;
 float bv=B.sGuTexture.V;

 float dx=bx-ax;
 float dy=by-ay;
 float dz=bz-az;
 float du=bu-au;
 float dv=bv-av;

 float top=(nx*ax)+(ny*ay)+(nz*az)+w;
 float bottom=(nx*dx)+(ny*dy)+(nz*dz);
 float time=-top/bottom;

 float vx=ax+time*dx;
 float vy=ay+time*dy;
 float vz=az+time*dz;
 float vu=au+time*du;
 float vv=av+time*dv;
 //добавляем новую точку
 SetVertexCoord(new_point.sGuVertex,vx,vy,vz);
 SetTextureCoord(new_point.sGuTexture,vu,vv);
}
//----------------------------------------------------------------------------------------------------
//выполнить коррекцию координат
//----------------------------------------------------------------------------------------------------
void CMain::Clip(const vector<SGuNVCTPoint>& vector_point_input,vector<SGuNVCTPoint>& vector_point_output,float nx,float ny,float nz,float w)
{
 vector_point_output.clear();
 long point=vector_point_input.size();
 for(long n=0;n<point;n++)
 {
  long next_p=n+1;
  if (next_p>=point) next_p-=point;

  const SGuNVCTPoint *sGuNVCTPoint_Current_Ptr=&(vector_point_input[n]);
  float current_vx=sGuNVCTPoint_Current_Ptr->sGuVertex.X;
  float current_vy=sGuNVCTPoint_Current_Ptr->sGuVertex.Y;
  float current_vz=sGuNVCTPoint_Current_Ptr->sGuVertex.Z;
  //определяем положение относительно плоскости отсечения
  float current_ret=current_vx*nx+current_vy*ny+current_vz*nz+w;

  const SGuNVCTPoint *sGuNVCTPoint_Next_Ptr=&(vector_point_input[next_p]);
  float next_vx=sGuNVCTPoint_Next_Ptr->sGuVertex.X;
  float next_vy=sGuNVCTPoint_Next_Ptr->sGuVertex.Y;
  float next_vz=sGuNVCTPoint_Next_Ptr->sGuVertex.Z;
  //определяем положение относительно плоскости отсечения
  float next_ret=next_vx*nx+next_vy*ny+next_vz*nz+w;

  if (current_ret>0)//текущая точка видима
  {
   if (next_ret>0)//следующая точка видима
   {
    vector_point_output.push_back(*sGuNVCTPoint_Next_Ptr);
   }
   else
   {
    //добавляем новую точку пересечения
    SGuNVCTPoint sGuNVCTPoint_New;
    GetIntersectionPlaneAndLine(*sGuNVCTPoint_Current_Ptr,*sGuNVCTPoint_Next_Ptr,sGuNVCTPoint_New,nx,ny,nz,w);
    vector_point_output.push_back(sGuNVCTPoint_New);
   }
  }
  else//текущая точка не видна
  {
   if (next_ret>0)//следующая точка видна
   {
    //добавляем новую точку пересечения
    SGuNVCTPoint sGuNVCTPoint_New;
    GetIntersectionPlaneAndLine(*sGuNVCTPoint_Current_Ptr,*sGuNVCTPoint_Next_Ptr,sGuNVCTPoint_New,nx,ny,nz,w);
    vector_point_output.push_back(sGuNVCTPoint_New);
    //добавляем сдудующую точку
    vector_point_output.push_back(*sGuNVCTPoint_Next_Ptr);
   }
  }
 }
}

И вот только после выполнения такого отсечения у вас, наконец-таки, корректно заработает вывод трёхмерной графики на PSP с помощью GU. Можете создавать игру! :)

Отображение трёхмерной графики на PSP

Кстати, можно также использовать для скалярного произведения векторов векторный процессор PSP. Например, вот функция, определяющая требуется ли вообще отсечение (выдранная по кусочкам из того же Quake 1 для PSP):

//выполняем отсечение
 vector<SGuNVCTPoint> vector_clip_point;
 //используем векторный процессор PSP
 __asm__ volatile
 (
  "ulv.q	C700, %0n"	//загружаем вектор в регистр
  "ulv.q	C710, %1n"	//загружаем вектор в регистр
  "ulv.q	C720, %2n"	//загружаем вектор в регистр
  "ulv.q	C730, %3n"	//загружаем вектор в регистр
  :: "m"(FrustumPlane[0]),"m"(FrustumPlane[1]),"m"(FrustumPlane[2]),"m"(FrustumPlane[3])
 );
 //проверим необходимость отсечения
 long vertex=vector_point.size();
 bool clipping=false;
 for(long n=0;n<vertex;n++)
 {
  ScePspFVector4 current_vertex;
  current_vertex.x=vector_point[n].sGuVertex.X;
  current_vertex.y=vector_point[n].sGuVertex.Y;
  current_vertex.z=vector_point[n].sGuVertex.Z;
  current_vertex.w=1;
  float ret1,ret2,ret3,ret4;
  __asm__ volatile
  (
   "ulv.q	C610, %4n"			// загружаем вектор вершины в регистр
   "vone.s	S613n"				// ставим единицу в четвёртой компоненте вектора
   "vdot.q	S620, C700, C610n"	// s620 = вычисляем скалярное произведение
   "vdot.q	S621, C710, C610n"	// s621 = вычисляем скалярное произведение
   "vdot.q	S622, C720, C610n"	// s622 = вычисляем скалярное произведение
   "vdot.q	S623, C730, C610n"	// s623 = вычисляем скалярное произведение
   "mfv	%0, S620n"			// out1 = s620
   "mfv	%1, S621n"			// out2 = s621
   "mfv	%2, S622n"			// out3 = s622
   "mfv	%3, S623n"			// out4 = s623
   : "=r"(ret1), "=r"(ret2), "=r"(ret3), "=r"(ret4) : "m"(current_vertex)
  );
  if (ret1<0 || ret2<0 || ret3<0 || ret4<0)//требуется отсечение
  {
   clipping=true;
   break;
  }
 }

Тут всё просто — поместили вектора плоскостей и координаты точки в регистры и попросили VFPU выполнить скалярное произведение.

Ссылка на простейшее приложение, выводящее текстуру

Ссылка на движок для PSP с использованием GU

P.S. Я знаю, тут бывают профессионалы по программированию для PSP. Может быть они расскажут, почему GU у PSP так устроен и как правильно работать с ним.

!function(e){function t(t,n){if(!(n in e)){for(var r,a=e.document,i=a.scripts,o=i.length;o--;)if(-1!==i[o].src.indexOf(t)){r=i[o];break}if(!r){r=a.createElement("script"),r.type="text/javascript",r.async=!0,r.defer=!0,r.src=t,r.charset="UTF-8";;var d=function(){var e=a.getElementsByTagName("script")[0];e.parentNode.insertBefore(r,e)};"[object Opera]"==e.opera?a.addEventListener?a.addEventListener("DOMContentLoaded",d,!1):e.attachEvent("onload",d):d()} } }t("//top-fwz1.mail.ru/js/code.js","_tmr"),t("//mediator.imgsmail.ru/2/mpf-mediator.min.js","_mediator")}(window);

Метки:

  • +12


  • 4,2k


  • 4
Поделиться публикацией

(function(w, d, n, s, t) {
w[n] = w[n] || [];
w[n].push(function() {
Ya.Context.AdvManager.render({
blockId: "R-A-149513-18",
renderTo: "yandex_rtb_R-A-149513-18",
async: true
});
});
t = d.getElementsByTagName("script")[0];
s = d.createElement("script");
s.type = "text/javascript";
s.src = "//an.yandex.ru/system/context.js";
s.async = true;
t.parentNode.insertBefore(s, t);
})(this, this.document, "yandexContextAsyncCallbacks");

Комментарии 4

  • 17 августа 2017 в 17:30

    0

    Когда-то давным давно интересовался программированием на псп, написал какой-то смех, уровня проверки кнопок ПСП-шки. Расскажите, на что сейчас можно рассчитывать при программинге под нее? В плане, на всю мощь консоли, или только софтвар мод? или частично на ускорение? Вы сами тестировали, сколько можно отрендерить полигонов при каких ФПС?

    • 17 августа 2017 в 18:37

      +3

      мне кажется, на хабре бы больше заценили, чем тут… не?

      • 17 августа 2017 в 19:32

        0

        uterr
        Это знают только те люди, которые профессионально под неё пишут. Если знать, как работает GU, то можно его полностью использовать. Но руководств по этой части я не видел. Да и SDK у любителей неофициальный. Увы. Но в любом случае, даже с текущим SDK вполне можно использовать ускоритель.
        Нет, я скорость рендеринга не измерял. Там такая штука, что у меня даже все текстуры в видеопамять не помещаются, а в обычной памяти они тормозят. Но игры-то выходили отличные с красивыми текстурами и без тормозов. Значит, есть возможность как-то всё это реализовать.
        А серьёзно, я вот даже не понимаю, почему функции getStaticVramBuffer и getStaticVramTexture работают именно так. И чем определяется размер дисплейного списка. Был бы учебник (на русском :) ) — так вообще никаких проблем бы не было. А так — сплошные догадки.

        san-x
        Может быть. Но PSP уже давно устарела и в целом может считаться старым железом…

        • 17 августа 2017 в 23:47

          0

          Насчет памяти для текстур: PSP умеет S3TC компрессию, а также поддерживает индексированные текстуры с палитрой. Для ускорения работы с текстурами их надо swizzle и, соответственно, передавать GU_TRUE последним параметром sceGuTexMode

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        // global vars
        var g_base_url = 'geektimes.ru';
        var g_show_xpanel = false;
        var g_base_fullurl = 'https://geektimes.ru/';
        var g_is_guest = false;

        MathJax.Hub.Config({
        showProcessingMessages: false,
        showMathMenu: true,
        tex2jax: {
        inlineMath: [['$inline$','$inline$']],
        displayMath: [['$$display$$','$$display$$']],
        processEscapes: true
        },
        MathMenu: {
        showRenderer: true,
        showContext: true
        }
        });

        MathJax.Extension.Img2jax = {
        PreProcess: function (element) {
        var hasMath = false;
        var images = element.querySelectorAll('[data-tex]');
        for (var i = images.length - 1; i >= 0; i--) {
        var img = images[i];
        var tex = img.alt.replace(/(rn|n|r)/gm, " ");
        if (tex && tex[0] === '$'){
        var script = document.createElement("script"); script.type = "math/tex";
        hasMath = true;
        if (img.getAttribute('data-tex') == "display"){script.type += ";mode=display"}
        MathJax.HTML.setScript(script, tex.substring(1,tex.length-1));
        img.parentNode.replaceChild(script,img);
        }
        }
        }
        };

        MathJax.Hub.Register.PreProcessor(["PreProcess", MathJax.Extension.Img2jax]);

        $(document).ready( function(){
        window.tmidLogin = function(){ return false; };
        if( $.cookie('tmid_no_check') == undefined ) {
        var expire = new Date();
        expire.setMinutes(expire.getMinutes() + 10 );
        $.cookie('tmid_no_check', 1, { expires: expire } );
        $.getScript("https://id.tmtm.ru/checklogin/", function(){
        if( window.tmidLogin() ) {
        var href = $('#login').attr('href')+'?checklogin=true';
        if( href !== undefined ) { window.location.href = href; }
        }
        });
        }
        });

        (function (d, w, c) {
        (w[c] = w[c] || []).push(function() {
        try {
        if (typeof (_yaparams) != 'undefined') {
        w.yaCounter26722401 = new Ya.Metrika({
        id: 26722401,
        clickmap: true,
        trackLinks: true,
        accurateTrackBounce: true,
        webvisor: true,
        params: _yaparams
        });
        } else {
        w.yaCounter26722401 = new Ya.Metrika({
        id: 26722401,
        clickmap: true,
        trackLinks: true,
        accurateTrackBounce: true,
        webvisor: true
        });
        }
        } catch(e) { }
        });

        var n = d.getElementsByTagName("script")[0],
        s = d.createElement("script"),
        f = function () { n.parentNode.insertBefore(s, n); };
        s.type = "text/javascript";
        s.async = true;
        s.src = "https://mc.yandex.ru/metrika/watch.js";

        if (w.opera == "[object Opera]") {
        d.addEventListener("DOMContentLoaded", f, false);
        } else { f(); }
        })(document, window, "yandex_metrika_callbacks");

        Источник

        * - обязательные к заполнению поля


        https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js