介绍
SFML提供了简单的类来表示最常见的2D实体。虽然更复杂的实体可以轻松地从这些基本组件创建,但这并不总是最有效的解决方案。例如,如果绘制大量的精灵,则很快会达到图形卡的限制。原因是性能在很大程度上取决于调用绘制函数的次数。确实,每次调用都涉及设置一组OpenGL状态,重置矩阵,更改纹理等等。即使仅绘制两个三角形(精灵),所有这些都是必需的。这对于您的图形卡来说远非最佳:今天的GPU设计用于处理大量的三角形,通常是几千到数百万个。
为了填补这一差距,SFML提供了一种更低级别的机制来绘制东西:顶点数组。实际上,顶点数组在所有其他SFML类中都被内部使用。它们允许更灵活地定义2D实体,包含您需要的任意数量的三角形。它们甚至允许绘制点或线条。
什么是顶点,为什么它们总是在数组中?
顶点是您可以操作的最小图形实体。简而言之,它是一个图形点:它自然具有2D位置(x,y),但也具有颜色和一对纹理坐标。稍后我们将讨论这些属性的作用。
单独的顶点并不能做太多事情。它们总是被组合成图元:点(1个顶点),线(2个顶点),三角形(3个顶点)或四边形(4个顶点)。然后,您可以将多个图元组合在一起,创建实体的最终几何形状。
现在您理解了为什么我们总是谈论顶点数组,而不是单独的顶点。
一个简单的顶点数组
现在让我们看看sf :: Vertex类。它只是一个容器,包含三个公共成员和除构造函数之外没有任何函数。这些构造函数允许您从您关心的属性集中构造顶点 - 您并不总是需要为实体着色或打上纹理。
// create a new vertex
sf::Vertex vertex;
// set its position
vertex.position = sf::Vector2f(10.f, 50.f);
// set its color
vertex.color = sf::Color::Red;
// set its texture coordinates
vertex.texCoords = sf::Vector2f(100.f, 100.f);
…或者,使用正确的构造函数:
sf::Vertex vertex(sf::Vector2f(10.f, 50.f), sf::Color::Red, sf::Vector2f(100.f, 100.f));
现在,让我们定义一个图元。请记住,一个图元由多个顶点组成,因此我们需要一个顶点数组。SFML为此提供了一个简单的包装器:sf :: VertexArray。它提供了类似于std :: vector的数组语义,并且还存储其顶点定义的图元类型。
// create an array of 3 vertices that define a triangle primitive
sf::VertexArray triangle(sf::Triangles, 3);
// define the position of the triangle's points
triangle[0].position = sf::Vector2f(10.f, 10.f);
triangle[1].position = sf::Vector2f(100.f, 10.f);
triangle[2].position = sf::Vector2f(100.f, 100.f);
// define the color of the triangle's points
triangle[0].color = sf::Color::Red;
triangle[1].color = sf::Color::Blue;
triangle[2].color = sf::Color::Green;
// no texture coordinates here, we'll see that later
你的三角形已经准备好,现在可以开始绘制它了。绘制顶点数组与绘制任何其他SFML实体类似,可以使用draw函数:
window.draw(triangle);
可以看到,顶点的颜色被插值以填充图元。这是创建渐变的不错方式。
请注意,你不一定要使用sf::VertexArray类,它只是为方便而定义的,它本质上只是一个带有sf::PrimitiveType的std::vector< sf::Vertex >。如果你需要更多的灵活性或者静态数组,你可以使用自己的存储方式。然后,你必须使用draw函数的重载形式,该函数接受指向顶点、顶点数和图元类型的指针。
std::vector<sf::Vertex> vertices;
vertices.push_back(sf::Vertex(...));
...
window.draw(&vertices[0], vertices.size(), sf::Triangles);
sf::Vertex vertices[2] =
{
sf::Vertex(...),
sf::Vertex(...)
};
window.draw(vertices, 2, sf::Lines);
图元类型
让我们暂停一下,看看你可以创建哪些类型的基本几何图元。如上所述,你可以定义最基本的2D图元:点、线、三角形和四边形(四边形只是为了方便起见,内部图形卡会将其分成两个三角形)。还有“链式”变体,它们允许在两个连续的图元之间共享顶点。这样做的好处是,连续的图元通常在某种程度上是相互连接的。
让我们来看看完整的列表:
图元类型 | 描述 | 例子 |
---|---|---|
sf::Points | 一组不相连的点。这些点没有厚度:无论当前的变换和视图如何,它们始终只占用一个像素。 | |
sf::Lines | 一组不相连的线。这些线没有厚度:无论当前的变换和视图如何,它们始终只有一个像素宽。 | |
sf::LineStrip | 一组相连的线。一个线段的结束顶点被用作下一个线段的起始顶点。 | |
sf::Triangles | 一组不相连的三角形。 | |
sf::TriangleStrip | 一组相连的三角形。每个三角形与下一个三角形共享其最后两个顶点。 | |
sf::TriangleFan | 一组连接到一个中心点的三角形。第一个顶点是中心,然后每个新顶点定义一个新的三角形,使用中心和前一个顶点。 | |
sf::Quads (已弃用) | 一组未连接的四边形。每个四边形的4个点必须一致地定义,可以顺时针或逆时针顺序。 |
纹理
与其他SFML实体一样,顶点数组也可以进行纹理贴图。为此,您需要操作顶点的texCoords属性。该属性定义了哪个纹理像素被映射到顶点。
// create a triangle strip
sf::VertexArray triangleStrip(sf::TriangleStrip, 4);
// define it as a rectangle, located at (10, 10) and with size 100x100
triangleStrip[0].position = sf::Vector2f(10.f, 10.f);
triangleStrip[1].position = sf::Vector2f(10.f, 110.f);
triangleStrip[2].position = sf::Vector2f(110.f, 10.f);
triangleStrip[3].position = sf::Vector2f(110.f, 110.f);
// define its texture area to be a 25x50 rectangle starting at (0, 0)
triangleStrip[0].texCoords = sf::Vector2f(0.f, 0.f);
triangleStrip[1].texCoords = sf::Vector2f(0.f, 50.f);
triangleStrip[2].texCoords = sf::Vector2f(25.f, 0.f);
triangleStrip[3].texCoords = sf::Vector2f(25.f, 50.f);
纹理坐标是以像素为单位定义的(就像精灵和形状的textureRect一样)。它们没有规范化(在0到1之间),这可能会让习惯于OpenGL编程的人感到惊讶。
顶点数组是低级实体,只处理几何数据,不存储像纹理这样的其他属性。要使用纹理绘制一个顶点数组,必须将其直接传递给绘制函数:
sf::VertexArray vertices;
sf::Texture texture;
...
window.draw(vertices, &texture);
这是简短的版本,如果您需要传递其他渲染状态(例如混合模式或变换),则可以使用显式版本,该版本接受一个sf::RenderStates对象:
sf::VertexArray vertices;
sf::Texture texture;
...
sf::RenderStates states;
states.texture = &texture;
window.draw(vertices, states);
变换顶点数组
Transforming(变换)与纹理类似,变换信息并不储存在顶点数组中,你必须将其传递给绘制函数。在使用 sf::Transform
类进行变换时,它所作用的是整个顶点数组,不会修改原始顶点数据。因此,在绘制时,你需要将变换信息通过 sf::RenderStates
对象传递给绘制函数,以实现顶点数组的变换。
sf::VertexArray vertices;
sf::Transform transform;
...
window.draw(vertices, transform);
或者,如果你需要传递其它的渲染状态:
sf::VertexArray vertices;
sf::Transform transform;
...
sf::RenderStates states;
states.transform = transform;
window.draw(vertices, states);
如果想了解更多有关变换和sf::Transform类的内容,你可以阅读有关变换实体的教程。
创建一个类似SFML的实体
现在您知道如何定义自己的纹理/着色/变换实体,那么将其包装在类中不是很好吗?幸运的是,SFML为您提供了sf::Drawable和sf::Transformable基类,使这一点变得容易。这两个类是内置SFML实体sf::Sprite、sf::Text和sf::Shape的基础。
sf::Drawable是一个接口:它声明一个纯虚函数,没有成员或具体函数。继承sf::Drawable允许您以与SFML类相同的方式绘制类的实例:
class MyEntity : public sf::Drawable
{
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const;
};
MyEntity entity;
window.draw(entity); // internally calls entity.draw
请注意,这样做并不是强制性的,您也可以在您的类中拥有类似的draw函数,然后简单地使用entity.draw(window)进行调用。但是,使用sf::Drawable作为基类的方式更好、更一致。这也意味着,如果您计划存储一组可绘制的对象,您可以这样做而不需要任何额外的努力,因为所有可绘制的对象(SFML的和您的)都是从相同的类派生的。
另一个基类sf::Transformable没有虚函数。从它继承会自动将与其他SFML类相同的变换函数添加到您的类中(setPosition、setRotation、move、scale等)。您可以在转换实体的教程中了解更多关于这个类的知识。
使用这两个基类和一个顶点数组(在这个例子中,我们也会添加一个纹理),这是一个典型的类似SFML的图形类应该是这样的:
class MyEntity : public sf::Drawable, public sf::Transformable
{
public:
// add functions to play with the entity's geometry / colors / texturing...
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
{
// apply the entity's transform -- combine it with the one that was passed by the caller
states.transform *= getTransform(); // getTransform() is defined by sf::Transformable
// apply the texture
states.texture = &m_texture;
// you may also override states.shader or states.blendMode if you want
// draw the vertex array
target.draw(m_vertices, states);
}
sf::VertexArray m_vertices;
sf::Texture m_texture;
};
然后,您可以像使用内置的SFML类一样使用这个类:
MyEntity entity;
// you can transform it
entity.setPosition(10.f, 50.f);
entity.setRotation(45.f);
// you can draw it
window.draw(entity);
示例:瓷砖地图
有了以上所学的知识,我们可以创建一个封装瓷砖地图的类。整个地图将包含在一个顶点数组中,因此它的绘制速度将非常快。请注意,我们只有在整个瓷砖集合可以适合一个纹理时,才能应用这种策略。否则,我们至少需要使用一个纹理对应一个顶点数组。
以下是瓷砖地图类的声明:
class TileMap : public sf::Drawable, public sf::Transformable
{
public:
bool load(const std::string& tileset, sf::Vector2u tileSize, const int* tiles, unsigned int width, unsigned int height)
{
// load the tileset texture
if (!m_tileset.loadFromFile(tileset))
return false;
// resize the vertex array to fit the level size
m_vertices.setPrimitiveType(sf::Triangles);
m_vertices.resize(width * height * 6);
// populate the vertex array, with two triangles per tile
for (unsigned int i = 0; i < width; ++i)
for (unsigned int j = 0; j < height; ++j)
{
// get the current tile number
int tileNumber = tiles[i + j * width];
// find its position in the tileset texture
int tu = tileNumber % (m_tileset.getSize().x / tileSize.x);
int tv = tileNumber / (m_tileset.getSize().x / tileSize.x);
// get a pointer to the triangles' vertices of the current tile
sf::Vertex* triangles = &m_vertices[(i + j * width) * 6];
// define the 6 corners of the two triangles
triangles[0].position = sf::Vector2f(i * tileSize.x, j * tileSize.y);
triangles[1].position = sf::Vector2f((i + 1) * tileSize.x, j * tileSize.y);
triangles[2].position = sf::Vector2f(i * tileSize.x, (j + 1) * tileSize.y);
triangles[3].position = sf::Vector2f(i * tileSize.x, (j + 1) * tileSize.y);
triangles[4].position = sf::Vector2f((i + 1) * tileSize.x, j * tileSize.y);
triangles[5].position = sf::Vector2f((i + 1) * tileSize.x, (j + 1) * tileSize.y);
// define the 6 matching texture coordinates
triangles[0].texCoords = sf::Vector2f(tu * tileSize.x, tv * tileSize.y);
triangles[1].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y);
triangles[2].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y);
triangles[3].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y);
triangles[4].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y);
triangles[5].texCoords = sf::Vector2f((tu + 1) * tileSize.x, (tv + 1) * tileSize.y);
}
return true;
}
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
{
// apply the transform
states.transform *= getTransform();
// apply the tileset texture
states.texture = &m_tileset;
// draw the vertex array
target.draw(m_vertices, states);
}
sf::VertexArray m_vertices;
sf::Texture m_tileset;
};
现在我们来看使用这个类的应用程序:
int main()
{
// create the window
sf::RenderWindow window(sf::VideoMode(512, 256), "Tilemap");
// define the level with an array of tile indices
const int level[] =
{
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 0, 0, 0, 0,
1, 1, 0, 0, 0, 0, 0, 0, 3, 3, 3, 3, 3, 3, 3, 3,
0, 1, 0, 0, 2, 0, 3, 3, 3, 0, 1, 1, 1, 0, 0, 0,
0, 1, 1, 0, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2, 0, 0,
0, 0, 1, 0, 3, 0, 2, 2, 0, 0, 1, 1, 1, 1, 2, 0,
2, 0, 1, 0, 3, 0, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1,
0, 0, 1, 0, 3, 2, 2, 2, 0, 0, 0, 0, 1, 1, 1, 1,
};
// create the tilemap from the level definition
TileMap map;
if (!map.load("tileset.png", sf::Vector2u(32, 32), level, 16, 8))
return -1;
// run the main loop
while (window.isOpen())
{
// handle events
sf::Event event;
while (window.pollEvent(event))
{
if(event.type == sf::Event::Closed)
window.close();
}
// draw the map
window.clear();
window.draw(map);
window.display();
}
return 0;
}
你可以在这里下载用于这个tilemap示例的瓷砖集。
例子:粒子系统
这个第二个示例实现了另一个常见实体:粒子系统。这个非常简单,没有纹理,尽可能少的参数。它演示了使用sf::Points原语类型与动态顶点数组,在每个帧中都会发生变化。
class ParticleSystem : public sf::Drawable, public sf::Transformable
{
public:
ParticleSystem(unsigned int count) :
m_particles(count),
m_vertices(sf::Points, count),
m_lifetime(sf::seconds(3.f)),
m_emitter(0.f, 0.f)
{
}
void setEmitter(sf::Vector2f position)
{
m_emitter = position;
}
void update(sf::Time elapsed)
{
for (std::size_t i = 0; i < m_particles.size(); ++i)
{
// update the particle lifetime
Particle& p = m_particles[i];
p.lifetime -= elapsed;
// if the particle is dead, respawn it
if (p.lifetime <= sf::Time::Zero)
resetParticle(i);
// update the position of the corresponding vertex
m_vertices[i].position += p.velocity * elapsed.asSeconds();
// update the alpha (transparency) of the particle according to its lifetime
float ratio = p.lifetime.asSeconds() / m_lifetime.asSeconds();
m_vertices[i].color.a = static_cast<sf::Uint8>(ratio * 255);
}
}
private:
virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
{
// apply the transform
states.transform *= getTransform();
// our particles don't use a texture
states.texture = NULL;
// draw the vertex array
target.draw(m_vertices, states);
}
private:
struct Particle
{
sf::Vector2f velocity;
sf::Time lifetime;
};
void resetParticle(std::size_t index)
{
// give a random velocity and lifetime to the particle
float angle = (std::rand() % 360) * 3.14f / 180.f;
float speed = (std::rand() % 50) + 50.f;
m_particles[index].velocity = sf::Vector2f(std::cos(angle) * speed, std::sin(angle) * speed);
m_particles[index].lifetime = sf::milliseconds((std::rand() % 2000) + 1000);
// reset the position of the corresponding vertex
m_vertices[index].position = m_emitter;
}
std::vector<Particle> m_particles;
sf::VertexArray m_vertices;
sf::Time m_lifetime;
sf::Vector2f m_emitter;
};
还有一个使用它的小演示:
int main()
{
// create the window
sf::RenderWindow window(sf::VideoMode(512, 256), "Particles");
// create the particle system
ParticleSystem particles(1000);
// create a clock to track the elapsed time
sf::Clock clock;
// run the main loop
while (window.isOpen())
{
// handle events
sf::Event event;
while (window.pollEvent(event))
{
if(event.type == sf::Event::Closed)
window.close();
}
// make the particle system emitter follow the mouse
sf::Vector2i mouse = sf::Mouse::getPosition(window);
particles.setEmitter(window.mapPixelToCoords(mouse));
// update it
sf::Time elapsed = clock.restart();
particles.update(elapsed);
// draw it
window.clear();
window.draw(particles);
window.display();
}
return 0;
}