-
简介
这节课NeHe教我们实现了卡通纹理。卡通渲染(英语:Cel-shading或者Toon Shading)是一种去真实感的渲染方法,旨在使电脑生成的图像呈现出手绘般的效果。为了使图像可以与漫画或者卡通达到形似的效果,专业人员通常使用卡通渲染着色器进行处理。卡通渲染是在大约21世纪初期,作为计算机图形学的副产物出现的新技术,并且主要应用于电子游戏中;然而,它可以呈现出如手绘动画一样简洁明了的效果。更多卡通渲染的内容可以参考Wiki:卡通渲染
-
实现
首先我们需要创建模型,模型来自文件model.txt,读取该模型并生成节点:
BOOL ReadMesh () // Reads The Contents Of The "model.txt" File ( NEW )
{
FILE *In = fopen ("Data\\model.txt", "rb"); // Open The File ( NEW )
if (!In)
return FALSE; // Return FALSE If File Not Opened ( NEW )
fread (&polyNum, sizeof (int), 1, In); // Read The Header (i.e. Number Of Polygons) ( NEW )
polyData = new POLYGON [polyNum]; // Allocate The Memory ( NEW )
fread (&polyData[0], sizeof (POLYGON) * polyNum, 1, In); // Read In All Polygon Data ( NEW )
fclose (In); // Close The File ( NEW )
return TRUE; // It Worked ( NEW )
}
之后我们需要创建本课所实现的两种效果:
1.卡通渲染着色:
首先创建一维纹理:
osg::Image *image = new osg::Image;
image->allocateImage(32, 1, 1, GL_RGB, GL_FLOAT);
osg::Vec3 *dataPtr = (osg::Vec3*)(image->data());
for (int i = 0; i < 32; ++i)
{
*dataPtr++ = osg::Vec3(shaderData[i][0], shaderData[i][1], shaderData[i][2]);
}
g_Texture1D = new osg::Texture1D;
g_Texture1D->setFilter(osg::Texture::MAG_FILTER, osg::Texture::NEAREST);
g_Texture1D->setFilter(osg::Texture::MIN_FILTER, osg::Texture::NEAREST);
g_Texture1D->setImage(image);
卡通着色的原理:用几何体的一条表面法线以及光线方向来找到光线撞击这个模型表面的强度,这两个向量的点积产生一个在0-1之间的值,可以将这个值作为一维纹理坐标来使用。
在节点的更新回调中计算纹理坐标(纹理坐标计算需要法线方向和光线方向):光线的方向一直不变是(0,0,1),而法线方向随着旋转会改变,我们通过获取旋转量来计算法线的实时方向,然后通过二者点乘得到纹理坐标,实现如下:
osg::FloatArray *textureArray = new osg::FloatArray;
for (int i = 0; i < polyNum; i++)
{
for (int j = 0; j < 3; j++)
{
osg::Vec3 nor;
nor.x() = polyData[i].Verts[j].Nor.X;
nor.y() = polyData[i].Verts[j].Nor.Y;
nor.z() = polyData[i].Verts[j].Nor.Z;
RotAxisCallback *rotCallback = dynamic_cast<RotAxisCallback*>(g_RotMT->getUpdateCallback());
double angle = rotCallback->getCurrentAngle();
nor = nor * osg::Matrix::rotate(angle, osg::Y_AXIS);
nor.normalize();
osg::Vec3 lightDir = osg::Vec3(lightAngle.X, lightAngle.Y, lightAngle.Z);
float TmpShade = nor * lightDir;
if (TmpShade < 0.0f)
TmpShade = 0.0f;
textureArray->push_back(TmpShade);
}
}
之后把节点加到场景中就完成卡通着色的效果。
2.Outline外框
实质上是通过一系列状态的组合来得到这种效果:
osg::Geode *outlineModel = createModelGeode();
g_OutlineGeode = outlineModel;
osg::Geometry *d = dynamic_cast<osg::Geometry*>(outlineModel->getDrawable(0));
if(d)
{
osg::Vec3Array *vecArray = dynamic_cast<osg::Vec3Array*>(d->getColorArray());
vecArray->at(0).set(osg::Vec3(0,0,0));
d->setColorArray(vecArray, osg::Array::BIND_OVERALL);
}
osg::BlendFunc *blendFunc = new osg::BlendFunc(osg::BlendFunc::SRC_ALPHA, osg::BlendFunc::ONE_MINUS_SRC_ALPHA);
osg::PolygonMode *polygonMode = new osg::PolygonMode(osg::PolygonMode::BACK, osg::PolygonMode::LINE);
osg::LineWidth *lineWidth = new osg::LineWidth(3.0f);
osg::CullFace *cullFace = new osg::CullFace(osg::CullFace::FRONT);
outlineModel->getOrCreateStateSet()->setAttributeAndModes(blendFunc);
outlineModel->getOrCreateStateSet()->setAttributeAndModes(polygonMode);
outlineModel->getOrCreateStateSet()->setAttributeAndModes(lineWidth);
outlineModel->getOrCreateStateSet()->setAttributeAndModes(cullFace);
在与键盘的交互代码中完成旋转和变换边框线的宽度:
case(osgGA::GUIEventAdapter::KEYDOWN):
{
if (ea.getKey() == osgGA::GUIEventAdapter::KEY_Space)
{
static bool flag = true;
if (!g_RotMT)
return false;
RotAxisCallback *rotCallback = dynamic_cast<RotAxisCallback*>(g_RotMT->getUpdateCallback());
if (!rotCallback)
return false;
if(flag)
{
double speed = rotCallback->getRotateSpeed();
speed -= 0.02;
rotCallback->setRotateSpeed(speed);
}
else
{
rotCallback->setRotateSpeed(0);
}
flag = !flag;
}
if (ea.getKey() == osgGA::GUIEventAdapter::KEY_Up)
{
if (!g_OutlineGeode)
return false;
osg::LineWidth *lineWidth = dynamic_cast<osg::LineWidth*>( g_OutlineGeode->getOrCreateStateSet()->getAttribute(osg::StateAttribute::LINEWIDTH));
float w = lineWidth->getWidth();
w += 1.0f;
lineWidth->setWidth(w);
}
if (ea.getKey() == osgGA::GUIEventAdapter::KEY_Down)
{
if (!g_OutlineGeode)
return false;
osg::LineWidth *lineWidth = dynamic_cast<osg::LineWidth*>(g_OutlineGeode->getOrCreateStateSet()->getAttribute(osg::StateAttribute::LINEWIDTH));
float w = lineWidth->getWidth();
w -= 1.0f;
lineWidth->setWidth(w);
}
编译运行程序:
附:本课源码(源码中可能存在错误和不足,仅供参考)
#include "../osgNeHe.h"
#include <QtCore/QTimer>
#include <QtGui/QApplication>
#include <QtGui/QVBoxLayout>
#include <osgViewer/Viewer>
#include <osgDB/ReadFile>
#include <osgQt/GraphicsWindowQt>
#include <osg/MatrixTransform>
#include <osg/Texture1D>
#include <osg/BlendFunc>
#include <osg/CullFace>
#include <osg/PolygonMode>
#include <osg/LineWidth>
// User Defined Structures
typedef struct tagMATRIX // A Structure To Hold An OpenGL Matrix ( NEW )
{
float Data[16]; // We Use [16] Due To OpenGL's Matrix Format ( NEW )
}
MATRIX;
typedef struct tagVECTOR // A Structure To Hold A Single Vector ( NEW )
{
float X, Y, Z; // The Components Of The Vector ( NEW )
}
VECTOR;
typedef struct tagVERTEX // A Structure To Hold A Single Vertex ( NEW )
{
VECTOR Nor; // Vertex Normal ( NEW )
VECTOR Pos; // Vertex Position ( NEW )
}
VERTEX;
typedef struct tagPOLYGON // A Structure To Hold A Single Polygon ( NEW )
{
VERTEX Verts[3]; // Array Of 3 VERTEX Structures ( NEW )
}
POLYGON;
// User Defined Variables
bool outlineDraw = true; // Flag To Draw The Outline ( NEW )
bool outlineSmooth = false; // Flag To Anti-Alias The Lines ( NEW )
float outlineColor[3] = { 0.0f, 0.0f, 0.0f }; // Color Of The Lines ( NEW )
float outlineWidth = 3.0f; // Width Of The Lines ( NEW )
VECTOR lightAngle; // The Direction Of The Light ( NEW )
bool lightRotate = false; // Flag To See If We Rotate The Light ( NEW )
float modelAngle = 0.0f; // Y-Axis Angle Of The Model ( NEW )
bool modelRotate = false; // Flag To Rotate The Model ( NEW )
POLYGON *polyData = NULL; // Polygon Data ( NEW )
int polyNum = 0; // Number Of Polygons ( NEW )
//GLuint shaderTexture[1]; // Storage For One Texture ( NEW )
osg::Texture1D *g_Texture1D;
osg::MatrixTransform *g_RotMT = NULL;
osg::Geode *g_OutlineGeode = NULL;
// File Functions
BOOL ReadMesh () // Reads The Contents Of The "model.txt" File ( NEW )
{
FILE *In = fopen ("Data\\model.txt", "rb"); // Open The File ( NEW )
if (!In)
return FALSE; // Return FALSE If File Not Opened ( NEW )
fread (&polyNum, sizeof (int), 1, In); // Read The Header (i.e. Number Of Polygons) ( NEW )
polyData = new POLYGON [polyNum]; // Allocate The Memory ( NEW )
fread (&polyData[0], sizeof (POLYGON) * polyNum, 1, In); // Read In All Polygon Data ( NEW )
fclose (In); // Close The File ( NEW )
return TRUE; // It Worked ( NEW )
}
// Math Functions
inline float DotProduct (VECTOR &V1, VECTOR &V2) // Calculate The Angle Between The 2 Vectors ( NEW )
{
return V1.X * V2.X + V1.Y * V2.Y + V1.Z * V2.Z; // Return The Angle ( NEW )
}
inline float Magnitude (VECTOR &V) // Calculate The Length Of The Vector ( NEW )
{
return sqrtf (V.X * V.X + V.Y * V.Y + V.Z * V.Z); // Return The Length Of The Vector ( NEW )
}
void Normalize (VECTOR &V) // Creates A Vector With A Unit Length Of 1 ( NEW )
{
float M = Magnitude (V); // Calculate The Length Of The Vector ( NEW )
if (M != 0.0f) // Make Sure We Don't Divide By 0 ( NEW )
{
V.X /= M; // Normalize The 3 Components ( NEW )
V.Y /= M;
V.Z /= M;
}
}
void RotateVector (MATRIX &M, VECTOR &V, VECTOR &D) // Rotate A Vector Using The Supplied Matrix ( NEW )
{
D.X = (M.Data[0] * V.X) + (M.Data[4] * V.Y) + (M.Data[8] * V.Z); // Rotate Around The X Axis ( NEW )
D.Y = (M.Data[1] * V.X) + (M.Data[5] * V.Y) + (M.Data[9] * V.Z); // Rotate Around The Y Axis ( NEW )
D.Z = (M.Data[2] * V.X) + (M.Data[6] * V.Y) + (M.Data[10] * V.Z); // Rotate Around The Z Axis ( NEW )
}
bool initialize()
{
int i;
char Line[255];
float shaderData[32][3];
FILE *In = NULL;
In = fopen ("Data\\shader.txt", "r");
if (In)
{
for (i = 0; i < 32; i++)
{
if (feof (In))
break;
fgets (Line, 255, In);
shaderData[i][0] = shaderData[i][1] = shaderData[i][2] = float(atof (Line));
}
fclose (In);
}
else
return false;
osg::Image *image = new osg::Image;
image->allocateImage(32, 1, 1, GL_RGB, GL_FLOAT);
osg::Vec3 *dataPtr = (osg::Vec3*)(image->data());
for (int i = 0; i < 32; ++i)
{
*dataPtr++ = osg::Vec3(shaderData[i][0], shaderData[i][1], shaderData[i][2]);
}
g_Texture1D = new osg::Texture1D;
g_Texture1D->setFilter(osg::Texture::MAG_FILTER, osg::Texture::NEAREST);
g_Texture1D->setFilter(osg::Texture::MIN_FILTER, osg::Texture::NEAREST);
g_Texture1D->setImage(image);
lightAngle.X = 0.0f;
lightAngle.Y = 0.0f;
lightAngle.Z = 1.0f;
Normalize (lightAngle);
return ReadMesh ();
}
osg::Geode* createModelGeode()
{
osg::Geode *geode = new osg::Geode;
osg::Geometry *geometry = new osg::Geometry;
osg::Vec3Array *vertexArray = new osg::Vec3Array;
osg::Vec3Array *colorArray = new osg::Vec3Array;
colorArray->push_back(osg::Vec3(1.0, 1.0, 1.0));
geometry->setColorArray(colorArray, osg::Array::BIND_OVERALL);
for (int i = 0; i < polyNum; i++)
{
for (int j = 0; j < 3; j++)
{
vertexArray->push_back(osg::Vec3(polyData[i].Verts[j].Pos.X,polyData[i].Verts[j].Pos.Y,polyData[i].Verts[j].Pos.Z));
}
}
geometry->setVertexArray(vertexArray);
geometry->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
geometry->addPrimitiveSet(new osg::DrawArrays(osg::PrimitiveSet::TRIANGLES, 0, vertexArray->size()));
geode->addDrawable(geometry);
return geode;
}
class RotAxisCallback : public osg::NodeCallback
{
public:
RotAxisCallback(const osg::Vec3& axis, double rotSpeed = 0.0, double currentAngle = 0.0)
: _rotAxis(axis), _rotSpeed(rotSpeed), _currentAngle(currentAngle){ }
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
osg::MatrixTransform *rotMT = dynamic_cast<osg::MatrixTransform*>(node);
if (!rotMT)
return;
rotMT->setMatrix(osg::Matrix::rotate(_currentAngle, _rotAxis));
_currentAngle += _rotSpeed;
traverse(node, nv);
}
void setRotateSpeed(double speed)
{
_rotSpeed = speed;
}
double getRotateSpeed() const
{
return _rotSpeed;
}
double getCurrentAngle() const
{
return _currentAngle;
}
private:
osg::Vec3 _rotAxis;
double _currentAngle;
double _rotSpeed;
};
//
class ManipulatorSceneHandler : public osgGA::GUIEventHandler
{
public:
ManipulatorSceneHandler()
{
}
virtual bool handle(const osgGA::GUIEventAdapter& ea, osgGA::GUIActionAdapter& aa)
{
osgViewer::Viewer *viewer = dynamic_cast<osgViewer::Viewer*>(&aa);
if (!viewer)
return false;
if (!viewer->getSceneData())
return false;
if (ea.getHandled())
return false;
osg::Group *root = viewer->getSceneData()->asGroup();
switch(ea.getEventType())
{
case(osgGA::GUIEventAdapter::KEYDOWN):
{
if (ea.getKey() == osgGA::GUIEventAdapter::KEY_Space)
{
static bool flag = true;
if (!g_RotMT)
return false;
RotAxisCallback *rotCallback = dynamic_cast<RotAxisCallback*>(g_RotMT->getUpdateCallback());
if (!rotCallback)
return false;
if(flag)
{
double speed = rotCallback->getRotateSpeed();
speed -= 0.02;
rotCallback->setRotateSpeed(speed);
}
else
{
rotCallback->setRotateSpeed(0);
}
flag = !flag;
}
if (ea.getKey() == osgGA::GUIEventAdapter::KEY_Up)
{
if (!g_OutlineGeode)
return false;
osg::LineWidth *lineWidth = dynamic_cast<osg::LineWidth*>( g_OutlineGeode->getOrCreateStateSet()->getAttribute(osg::StateAttribute::LINEWIDTH));
float w = lineWidth->getWidth();
w += 1.0f;
lineWidth->setWidth(w);
}
if (ea.getKey() == osgGA::GUIEventAdapter::KEY_Down)
{
if (!g_OutlineGeode)
return false;
osg::LineWidth *lineWidth = dynamic_cast<osg::LineWidth*>(g_OutlineGeode->getOrCreateStateSet()->getAttribute(osg::StateAttribute::LINEWIDTH));
float w = lineWidth->getWidth();
w -= 1.0f;
lineWidth->setWidth(w);
}
}
default: break;
}
return false;
}
};
class ToonShadingCallback : public osg::NodeCallback
{
public:
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
{
osg::Geode *geode = dynamic_cast<osg::Geode*>(node);
if(!geode)
return;
osg::Geometry *model = dynamic_cast<osg::Geometry*>(geode->getDrawable(0));
if (!model)
return;
osg::FloatArray *textureArray = new osg::FloatArray;
for (int i = 0; i < polyNum; i++)
{
for (int j = 0; j < 3; j++)
{
osg::Vec3 nor;
nor.x() = polyData[i].Verts[j].Nor.X;
nor.y() = polyData[i].Verts[j].Nor.Y;
nor.z() = polyData[i].Verts[j].Nor.Z;
RotAxisCallback *rotCallback = dynamic_cast<RotAxisCallback*>(g_RotMT->getUpdateCallback());
double angle = rotCallback->getCurrentAngle();
nor = nor * osg::Matrix::rotate(angle, osg::Y_AXIS);
nor.normalize();
osg::Vec3 lightDir = osg::Vec3(lightAngle.X, lightAngle.Y, lightAngle.Z);
float TmpShade = nor * lightDir;
if (TmpShade < 0.0f)
TmpShade = 0.0f;
textureArray->push_back(TmpShade);
}
}
model->setTexCoordArray(0, textureArray, osg::Array::BIND_PER_VERTEX);
model->getOrCreateStateSet()->setTextureAttributeAndModes(0, g_Texture1D);
traverse(node, nv);
}
};
//
//osgNeHe
//
class ViewerWidget : public QWidget, public osgViewer::Viewer
{
public:
ViewerWidget(osg::Node *scene = NULL)
{
QWidget* renderWidget = getRenderWidget( createGraphicsWindow(0,0,100,100), scene);
QVBoxLayout* layout = new QVBoxLayout;
layout->addWidget(renderWidget);
layout->setContentsMargins(0, 0, 0, 1);
setLayout( layout );
connect( &_timer, SIGNAL(timeout()), this, SLOT(update()) );
_timer.start( 10 );
}
QWidget* getRenderWidget( osgQt::GraphicsWindowQt* gw, osg::Node* scene )
{
osg::Camera* camera = this->getCamera();
camera->setGraphicsContext( gw );
const osg::GraphicsContext::Traits* traits = gw->getTraits();
camera->setClearColor( osg::Vec4(0.7f, 0.7f, 0.7f, 0.0f) );
camera->setViewport( new osg::Viewport(0, 0, traits->width, traits->height) );
camera->setProjectionMatrixAsPerspective(45.0f, static_cast<double>(traits->width)/static_cast<double>(traits->height), 0.1f, 100.0f );
camera->setViewMatrixAsLookAt(osg::Vec3d(0, 0, 0), osg::Vec3d(0, 0, -1), osg::Vec3d(0, 1, 0));
this->setSceneData( scene );
this->addEventHandler(new ManipulatorSceneHandler);
return gw->getGLWidget();
}
osgQt::GraphicsWindowQt* createGraphicsWindow( int x, int y, int w, int h, const std::string& name="", bool windowDecoration=false )
{
osg::DisplaySettings* ds = osg::DisplaySettings::instance().get();
osg::ref_ptr<osg::GraphicsContext::Traits> traits = new osg::GraphicsContext::Traits;
traits->windowName = name;
traits->windowDecoration = windowDecoration;
traits->x = x;
traits->y = y;
traits->width = w;
traits->height = h;
traits->doubleBuffer = true;
traits->alpha = ds->getMinimumNumAlphaBits();
traits->stencil = ds->getMinimumNumStencilBits();
traits->sampleBuffers = ds->getMultiSamples();
traits->samples = ds->getNumMultiSamples();
return new osgQt::GraphicsWindowQt(traits.get());
}
virtual void paintEvent( QPaintEvent* event )
{
frame();
}
protected:
QTimer _timer;
};
osg::Node* buildScene()
{
initialize();
osg::Group *root = new osg::Group;
osg::MatrixTransform *zoomMT = new osg::MatrixTransform;
zoomMT->setMatrix(osg::Matrix::translate(0,0,-2));
osg::MatrixTransform *rotMT = new osg::MatrixTransform;
g_RotMT = rotMT;
rotMT->addUpdateCallback(new RotAxisCallback(osg::Y_AXIS));
root->addChild(zoomMT);
zoomMT->addChild(rotMT);
//
//Cell Shading Model
osg::Geode *cellModel = createModelGeode();
cellModel->addUpdateCallback(new ToonShadingCallback);
rotMT->addChild(cellModel);
//
//Outline Model
osg::Geode *outlineModel = createModelGeode();
g_OutlineGeode = outlineModel;
osg::Geometry *d = dynamic_cast<osg::Geometry*>(outlineModel->getDrawable(0));
if(d)
{
osg::Vec3Array *vecArray = dynamic_cast<osg::Vec3Array*>(d->getColorArray());
vecArray->at(0).set(osg::Vec3(0,0,0));
d->setColorArray(vecArray, osg::Array::BIND_OVERALL);
}
osg::BlendFunc *blendFunc = new osg::BlendFunc(osg::BlendFunc::SRC_ALPHA, osg::BlendFunc::ONE_MINUS_SRC_ALPHA);
osg::PolygonMode *polygonMode = new osg::PolygonMode(osg::PolygonMode::BACK, osg::PolygonMode::LINE);
osg::LineWidth *lineWidth = new osg::LineWidth(3.0f);
osg::CullFace *cullFace = new osg::CullFace(osg::CullFace::FRONT);
outlineModel->getOrCreateStateSet()->setAttributeAndModes(blendFunc);
outlineModel->getOrCreateStateSet()->setAttributeAndModes(polygonMode);
outlineModel->getOrCreateStateSet()->setAttributeAndModes(lineWidth);
outlineModel->getOrCreateStateSet()->setAttributeAndModes(cullFace);
rotMT->addChild(outlineModel);
return root;
}
int main( int argc, char** argv )
{
QApplication app(argc, argv);
ViewerWidget* viewWidget = new ViewerWidget(buildScene());
viewWidget->setGeometry( 100, 100, 640, 480 );
viewWidget->show();
return app.exec();
}