当片段着色器处理完片段之后,模板测试(Stencil Test) 就开始执行了,和深度测试一样,它能丢弃一些片段。仍然保留下来的片段进入深度测试阶段,深度测试可能丢弃更多。模板测试基于模板缓冲(Stencil Buffer)。
模板缓冲中的模板值(Stencil Value)通常是8位的,因此每个片段/像素共有256种不同的模板值(译注:8位就是1字节大小,因此和char的容量一样是256个不同值)。这样我们就能将这些模板值设置为我们链接的,然后在模板测试时根据这个模板值,我们就可以决定丢弃或保留它了。
下面是一个模板缓冲的简单例子:
模板缓冲先清空模板缓冲设置所有片段的模板值为0,然后开启矩形片段用1填充。场景中的模板值为1的那些片段才会被渲染(其他的都被丢弃)。
无论我们在渲染哪里的片段,模板缓冲操作都允许我们把模板缓冲设置为一个特定值。改变模板缓冲的内容实际上就是对模板缓冲进行写入。在同一次(或接下来的)渲染迭代我们可以读取这些值来决定丢弃还是保留这些片段。当使用模板缓冲的时候,你可以随心所欲,但是需要遵守下面的原则:
- 开启模板缓冲写入。
- 渲染物体,更新模板缓冲。
- 关闭模板缓冲写入。
- 渲染(其他)物体,这次基于模板缓冲内容丢弃特定片段。
使用模板缓冲我们可以基于场景中已经绘制的片段,来决定是否丢弃特定的片段。
物体轮廓
要理解模板测试是如何工作的,我们会展示一个用模板测试实现的一个特别的和有用的功能,叫做 物体轮廓(Object Outlining) 。
物体轮廓就像它的名字所描述的那样,它能够给每个(或一个)物体创建一个有颜色的边。在策略游戏中当你打算选择一个单位的时候它特别有用。给物体加上轮廓的步骤如下:
- 在绘制物体前,把模板方程设置为
GL_ALWAYS
,用1更新物体将被渲染的片段。 - 渲染物体,写入模板缓冲。
- 关闭模板写入和深度测试。
- 每个物体放大一点点。
- 使用一个不同的片段着色器用来输出一个纯颜色。
- 再次绘制物体,但只是当它们的片段的模板值不为1时才进行。
- 开启模板写入和深度测试。
这个过程将每个物体的片段模板缓冲设置为1,当我们绘制边框的时候,我们基本上绘制的是放大版本的物体能通过测试的地方,放大的版本绘制后物体就会有一个边。我们基本会使用模板缓冲丢弃所有的不是原来物体的片段的放大的版本内容。
void main()
{
outColor = vec4(0.04, 0.28, 0.26, 1.0);
}
我们只打算给两个箱子加上边框,所以我们不会对地面做什么。这样我们要先绘制地面,然后再绘制两个箱子(同时写入模板缓冲),接着我们绘制放大的箱子(同时丢弃前面已经绘制的箱子的那部分片段)。
(二)、开启模板测试,设置模板、深度测试通过或失败时才采取动作:
//开启模板测试
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
//sfail: 如果模板测试失败将采取的动作。
//dpfail: 如果模板测试通过,但是深度测试失败时采取的动作。
//dppass: 如果深度测试和模板测试都通过,将采取的动作。
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
有两种函数可供我们使用去配置模板测试:glStencilFunc
和glStencilOp
。
void glStencilFunc(GLenum func, GLint ref, GLuint mask)
函数有三个参数:
- func:设置模板测试操作。这个测试操作应用到已经储存的模板值和
glStencilFunc
的ref
值上,可用的选项是:GL_NEVER
、GL_LEQUAL
、GL_GREATER
、GL_GEQUAL
、GL_EQUAL
、GL_NOTEQUAL
、GL_ALWAYS
。它们的语义和深度缓冲的相似。 - ref:指定模板测试的引用值。模板缓冲的内容会与这个值对比。
- mask:指定一个遮罩,在模板测试对比引用值和储存的模板值前,对它们进行按位与(and)操作,初始设置为1。因为模板缓冲中的模板值(Stencil Value)通常是8位, 0xFF相当于二进制的11111111。
GL_EQUAL
)引用值
1
,片段就能通过测试被绘制了,否则就会被丢弃。
glStencilOp 描述我们如何更新缓冲。
glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass)
函数包含三个选项,我们可以指定每个选项的动作:
- sfail: 如果模板测试失败将采取的动作。
- dpfail: 如果模板测试通过,但是深度测试失败时采取的动作。
- dppass: 如果深度测试和模板测试都通过,将采取的动作。
每个选项都可以使用下列任何一个动作。
操作 | 描述 |
---|---|
GL_KEEP | 保持现有的模板值 |
GL_ZERO | 将模板值置为0 |
GL_REPLACE | 将模板值设置为用glStencilFunc 函数设置的ref值 |
GL_INCR | 如果模板值不是最大值就将模板值+1 |
GL_INCR_WRAP | 与GL_INCR 一样将模板值+1,如果模板值已经是最大值则设为0 |
GL_DECR | 如果模板值不是最小值就将模板值-1 |
GL_DECR_WRAP | 与GL_DECR 一样将模板值-1,如果模板值已经是最小值则设为最大值 |
GL_INVERT | Bitwise inverts the current stencil buffer value. |
glStencilOp
函数默认设置为 (GL_KEEP, GL_KEEP, GL_KEEP) ,所以任何测试的任何结果,模板缓冲都会保留它的值。默认行为不会更新模板缓冲,所以如果你想写入模板缓冲的话,你必须像任意选项指定至少一个不同的动作。
使用glStencilFunc
和glStencilOp
,我们就可以指定在什么时候以及我们打算怎么样去更新模板缓冲了,我们也可以指定何时让测试通过或不通过。什么时候片段会被抛弃。
(三)、清空模板缓冲为0,为箱子的所有绘制的片段的模板缓冲更新为1:
glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要写入模板缓冲
glStencilMask(0xFF); // 设置模板缓冲为可写状态
normalShader.Use();
DrawTwoContainers();
使用
GL_ALWAYS
模板测试函数,我们确保箱子的每个片段用模板值1更新模板缓冲。因为片段总会通过模板测试,在我们绘制它们的地方,模板缓冲用引用值更新。
(四)、绘制放大的箱子,但是这次关闭模板缓冲的写入:
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); // 禁止修改模板缓冲
glDisable(GL_DEPTH_TEST);
shaderSingleColor.Use();
DrawTwoScaledUpContainers();
模板方程设置为GL_NOTEQUAL
,它保证我们只绘制箱子上不等于1的部分,这样只绘制前面绘制的箱子外围的那部分。注意,我们也要关闭深度测试,这样放大的的箱子也就是边框才不会被地面覆盖。做完之后还要保证再次开启深度缓冲。
场景中的物体边框的绘制方法最后看起来像这样:
glEnable(GL_DEPTH_TEST);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
glStencilMask(0x00); // 绘制地板时确保关闭模板缓冲的写入
normalShader.Use();
DrawFloor()
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilMask(0xFF);
DrawTwoContainers();
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);
glDisable(GL_DEPTH_TEST);
shaderSingleColor.Use();
DrawTwoScaledUpContainers();
glStencilMask(0xFF);
glEnable(GL_DEPTH_TEST);
全部代码如下:
//
// OpenGLStencilTest.cpp
// shaderTest
//
// Created by MacSBL on 2017/1/23.
//
//
#include "OpenGLStencilTest.h"
bool OpenGLStencilTest::init()
{
if (!Layer::init()) {
return false;
}
origin = Director::getInstance()->getVisibleOrigin();
vsize = Director::getInstance()->getVisibleSize();
GLfloat cubeVertices[] = {
// Positions // Texture Coords
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
GLfloat planeVertices[] = {
// Positions // Texture Coords (note we set these higher than 1 that together with GL_REPEAT as texture wrapping mode will cause the floor texture to repeat)
5.0f, -0.5f, 5.0f, 2.0f, 0.0f,
-5.0f, -0.5f, 5.0f, 0.0f, 0.0f,
-5.0f, -0.5f, -5.0f, 0.0f, 2.0f,
5.0f, -0.5f, 5.0f, 2.0f, 0.0f,
-5.0f, -0.5f, -5.0f, 0.0f, 2.0f,
5.0f, -0.5f, -5.0f, 2.0f, 2.0f
};
auto gprogram = GLProgram::createWithFilenames("box.vsh", "box.fsh");
this->setGLProgram(gprogram);
/
// box vao;
glGenVertexArrays(1, &boxvao);
glBindVertexArray(boxvao);
GLuint boxvbo;
glGenBuffers(1, &boxvbo);
glBindBuffer(GL_ARRAY_BUFFER, boxvbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), cubeVertices, GL_STATIC_DRAW);
//给vertex shader 的属性传值
GLuint posloc = glGetAttribLocation(gprogram->getProgram(), "a_position");
glEnableVertexAttribArray(posloc);
glVertexAttribPointer(posloc, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
GLuint texloc = glGetAttribLocation(gprogram->getProgram(), "a_texcoord");
glEnableVertexAttribArray(texloc);
glVertexAttribPointer(texloc, 2, GL_FLOAT, GL_FALSE, 5* sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
/
boxoutline_program = new GLProgram();
boxoutline_program->initWithFilenames("box.vsh", "singlecolor.fsh");
boxoutline_program->link();
//给vertex shader 的属性传值
GLuint ploc = glGetAttribLocation(boxoutline_program->getProgram(), "a_position");
glEnableVertexAttribArray(ploc);
glVertexAttribPointer(ploc, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
GLuint tloc = glGetAttribLocation(boxoutline_program->getProgram(), "a_texcoord");
glEnableVertexAttribArray(tloc);
glVertexAttribPointer(tloc, 2, GL_FLOAT, GL_FALSE, 5* sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glBindVertexArray(0);
/
//floor vao;
glGenVertexArrays(1, &floorvao);
glBindVertexArray(floorvao);
GLuint floorvbo;
glGenBuffers(1, &floorvbo);
glBindBuffer(GL_ARRAY_BUFFER, floorvbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(planeVertices), planeVertices, GL_STATIC_DRAW);
//给vertex shader 的属性传值
GLuint posloc2 = glGetAttribLocation(gprogram->getProgram(), "a_position");
glVertexAttribPointer(posloc2, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(posloc2);
GLuint texloc2 = glGetAttribLocation(gprogram->getProgram(), "a_texcoord");
glVertexAttribPointer(texloc2, 2, GL_FLOAT, GL_FALSE, 5* sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
glEnableVertexAttribArray(texloc2);
glBindVertexArray(0);
/
// 开启深度测试
Director::getInstance()->setDepthTest(true);
// glDepthFunc(GL_ALWAYS);
//开启模板测试
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
//sfail: 如果模板测试失败将采取的动作。
//dpfail: 如果模板测试通过,但是深度测试失败时采取的动作。
//dppass: 如果深度测试和模板测试都通过,将采取的动作。
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
//加载纹理
auto sprite = Sprite::create("container2.png");
boxTexture = sprite->getTexture()->getName();
auto floorimg = Sprite::create("wall.jpg");
floorTexture = floorimg->getTexture()->getName();
cam = new MyCamera(Vec3(0, 0, 3));
//touch事件
auto elistener = EventListenerTouchOneByOne::create();
elistener->onTouchBegan = CC_CALLBACK_2(OpenGLStencilTest::onTouchBegan, this);
elistener->onTouchMoved = CC_CALLBACK_2(OpenGLStencilTest::onTouchMoved, this);
elistener->onTouchEnded = CC_CALLBACK_2(OpenGLStencilTest::onTouchEnded, this);
_eventDispatcher->addEventListenerWithSceneGraphPriority(elistener, this);
return true;
}
void OpenGLStencilTest::visit(cocos2d::Renderer *render, const cocos2d::Mat4 &parentTransform, uint32_t parentflag)
{
Layer::visit(render, parentTransform, parentflag);
_command.init(_globalZOrder);
_command.func = CC_CALLBACK_0(OpenGLStencilTest::onDraw, this);
Director::getInstance()->getRenderer()->addCommand(&_command);
}
void OpenGLStencilTest::onDraw()
{
//清空模板缓冲为0,为箱子的所有绘制的片段的模板缓冲更新为1:
glStencilFunc(GL_ALWAYS, 1, 0xFF); //所有片段都要写入模板缓冲
glStencilMask(0xFF); //设置模板缓冲为可写状态
auto program = this->getGLProgram();
program->use();
auto view = cam->GetViewMatrix();
auto projection = new Mat4;
Mat4::createPerspective(cam->Zoom, vsize.width/vsize.height, 0.1, 1000.0f, projection);
//给vertex shader 的uniform 传值
glUniformMatrix4fv(glGetUniformLocation(program->getProgram(), "view"), 1, GL_FALSE,view->m);
glUniformMatrix4fv(program->getUniformLocation("projection"), 1, GL_FALSE, projection->m);
/
//box
glBindVertexArray(boxvao);
GL::bindTexture2DN(0, boxTexture);
glUniform1i(program->getUniformLocation("u_tex"), 0);
auto model = new Mat4();
model->translate(-1, 0, -1);
glUniformMatrix4fv(glGetUniformLocation(program->getProgram(), "model"), 1, GL_FALSE, model->m);
glDrawArrays(GL_TRIANGLES, 0, 36);
auto model2 = new Mat4;
model2->translate(1, 0, 0);
glUniformMatrix4fv(program->getUniformLocation("model"), 1, GL_FALSE, model2->m);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
/
glStencilMask(0x00); // 绘制地板时确保关闭模板缓冲的写入
//floor
glBindVertexArray(floorvao);
GL::bindTexture2D(floorTexture);
auto fmodel = new Mat4;
glUniformMatrix4fv(program->getUniformLocation("model"), 1, GL_FALSE, fmodel->m);
glDrawArrays(GL_TRIANGLES, 0, 6);
glBindVertexArray(0);
/
//箱子的外框, 是绘制放大的箱子
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00); 禁止修改模板缓冲
Director::getInstance()->setDepthTest(false);
boxoutline_program->use();
//给vertex shader 的uniform 传值
glUniformMatrix4fv(glGetUniformLocation(boxoutline_program->getProgram(), "view"), 1, GL_FALSE,view->m);
glUniformMatrix4fv(boxoutline_program->getUniformLocation("projection"), 1, GL_FALSE, projection->m);
glBindVertexArray(boxvao);
// GL::bindTexture2DN(0, boxTexture);
GLfloat fscale = 1.1;
model = new Mat4();
model->translate(-1, 0, -1);
model->scale(fscale);
glUniformMatrix4fv(glGetUniformLocation(boxoutline_program->getProgram(), "model"), 1, GL_FALSE, model->m);
glDrawArrays(GL_TRIANGLES, 0, 36);
model2 = new Mat4;
model2->translate(1, 0, 0);
model2->scale(fscale);
glUniformMatrix4fv(boxoutline_program->getUniformLocation("model"), 1, GL_FALSE, model2->m);
glDrawArrays(GL_TRIANGLES, 0, 36);
glBindVertexArray(0);
glStencilMask(0xFF);
Director::getInstance()->setDepthTest(true);
}
#pragma mark touch
bool OpenGLStencilTest::onTouchBegan(cocos2d::Touch *touch, cocos2d::Event *evt)
{
return true;
}
void OpenGLStencilTest::onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *evt)
{
Vec2 curpos = touch->getLocationInView();
Vec2 prepos = touch->getPreviousLocationInView();
GLfloat dx = curpos.x - prepos.x;
GLfloat dy = curpos.y - prepos.y;
//移动摄像机
GLfloat camspeed = 0.05f;
if (curpos.y - prepos.y > 0) { //w
cam->ProcessKeyboard(Cam_Move::FORWARD, camspeed);
}else if (curpos.y - prepos.y < 0){ //s
cam->ProcessKeyboard(Cam_Move::BACKWARD, camspeed);
}
else if (curpos.x - prepos.x < 0){ //a
cam->ProcessKeyboard(Cam_Move::LEFT, camspeed);
}else if (curpos.x - prepos.x > 0){ //d
cam->ProcessKeyboard(Cam_Move::RIGHT, camspeed);
}
//(3)旋转摄像机
// cam->ProcessMouseMovement(dx, dy);
//(4)缩放
// if(fov >= 1 && fov <= 45){
// fov -= dx * camspeed;
// }
// if(fov <= 1){
// fov = 1;
// }
// if(fov >= 45){
// fov = 45;
// }
}
void OpenGLStencilTest::onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *evt)
{
}