NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模
光、纹理和着色器是渲染状态的一部分,因此它们的处理方式与之前所示的一样。 当然,使用它们需要利用 osg::StateAttribute
的子类,如 osg::Light
、 osg::Texture2D
和 osg::Program
,但思路是相同的。 因此,只需花一些时间阅读 Open Scene Graph 参考文档即可。
可以用上面一段话就描述完关于灯光、纹理和着色器的信息,但 灯光、纹理和着色器有它们自己的怪癖、复杂性和差异,因此在实践中,仅花一些时间阅读参考文档并不足以理解它们。 因此,我们需要这篇文章。
1、灯光:位置状态
灯光与渲染状态的其余部分不完全相同的唯一原因是,灯光本质上是位置相关的:根据灯泡、蜡烛、火炬或篝火的位置,场景将呈现不同。
现在,想想看:位置是关键的区别,OSG 已经很好地支持表示场景中的位置(通过层级的变换节点,比如我们一直在使用的 osg::PositionAttitudeTransform
)。 因此,使用变换节点来设置光源的位置不是很有意义吗? 是的,这确实是 OSG 中的工作方式。 然而,考虑到 OpenGL 处理灯光的方式,我们最终得到了一些有点不直观的东西。
让我们从类似于之前看到的常规渲染状态的东西开始。 有一种渲染模式(GL_LIGHTING,我们在前面的示例程序中使用过)可用于完全禁用照明。 启用照明后,可以使用名为 GL_LIGHT0、GL_LIGHT1 等的模式独立启用或禁用各个光源。
因此,我们如何启用或禁用场景图或其子集上的单个灯光并没有什么特别之处。 两个悬而未决的问题是如何设置灯光参数(如颜色和方向)以及如何在场景中定位它们。 这是事情开始不同的地方。 这两个问题的答案都与节点类 osg::LightSource
相关,它扮演着几个不同的角色。
首先, osg::LightSource
链接了一个 osg::Light
的实例,它是控制光源属性的 osg::StateAtrribute
。 使用 osg::Light
的方法,我们可以设置光的颜色,或者将其配置为向所有方向发射光的点光源或聚光灯。 osg::Light
有另一个基本信息,即灯光编号。 灯光编号 n 只会照亮场景图中启用了 GL_LIGHTn
模式的部分。
其次, osg::LightSource
在场景中的位置(由出现在场景图的根和 osg::LightSource
本身的变换给出)决定了光源在世界中的位置。 好吧,差不多: osg::Light
也包含一个位置属性,但那里设置的位置是相对于 osg::LightSource
而言。 我通常将 osg::Light
的位置设置为 (0, 0, 0),以便只考虑 osg::LightSource
的位置。
所以,总结一下:当你想要在场景中使用光源时,可以将 osg::LightSource
添加到场景图中,以一种将其正确转换到你希望光源所在的位置的方式。然后,配置 osg::LightSource
提供的 osg::Light
的灯光参数; 特别要记住设置灯号。 这将照亮启用了相应 GL_LIGHTn
模式的场景图的所有部分。 最后,不要忘记禁用 GL_LIGHTING 模式,否则整个光照计算都会被绕过。
这有什么不直观的? 事实上,如果我们考虑一下, osg::LightSource
并不是真正在发光。 它只是设置光源的位置和属性。 如果你希望场景的某些部分被此光源照亮,仍然必须在所需的子图中启用相应的 GL_LIGHTn 模式。 是的,如果你想为整个场景启用一个灯,可以只在图的根节点启用模式——状态将被正常继承。
我要在这里给出最后一个实用技巧。 如果你查看 OSG 参考文档,会注意到 osg::LightSource
是一个 osg::Group
。 你能猜出为什么这有用吗? (如果你说“因为它所有的孩子都会被点亮”,到黑板前把前面的段落抄 100 遍!)这很有用,因为你可能想渲染一些几何体(比如灯泡)来代表你的光源 3D 世界——这就是添加它的地方。
经过冗长而乏味的讨论之后,是时候举个例子了。 我们将加载命令行传递的 3D 模型,用黄色光源点亮它,并显示一个 3D 对象以指示该光源的位置。 下图显示了示例的场景图。
// LightenedViewer.cpp
#include <iostream>
#include <osg/Light>
#include <osg/LightSource>
#include <osg/PositionAttitudeTransform>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
int main(int argc, char* argv[])
{
// Check command-line parameters
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <model file>\n";
exit(1);
}
// Load the model, create the bulk of the scene graph
osg::ref_ptr<osg::Group> sgRoot(new osg::Group());
osg::ref_ptr<osg::Node> loadedModel = osgDB::readNodeFile(argv[1]);
if (!loadedModel)
{
std::cerr << "Problem opening '" << argv[1] << "'\n";
exit(1);
}
sgRoot->addChild(loadedModel);
osg::ref_ptr<osg::Node> lampModel = osgDB::readNodeFile("round_lamp.obj");
if (!lampModel)
{
std::cerr << "Problem opening the lamp model.\n";
exit(1);
}
osg::ref_ptr<osg::PositionAttitudeTransform> lightPAT(
new osg::PositionAttitudeTransform());
lightPAT->setPosition(osg::Vec3(5.0, 12.0, 3.0));
sgRoot->addChild(lightPAT);
// Setup GL_LIGHT1. Leave GL_LIGHT0 as it is by default (enabled)
osg::ref_ptr<osg::LightSource> lightSource(new osg::LightSource()); // (1)
lightSource->addChild(lampModel); // (1)
lightSource->getLight()->setLightNum(1); // (1)
lightSource->getLight()->setPosition(osg::Vec4(0.0, 0.0, 0.0, 1.0)); // (1)
lightSource->getLight()->setDiffuse(osg::Vec4(1.0, 1.0, 0.0, 1.0)); // (1)
lightPAT->addChild(lightSource);
osg::ref_ptr<osg::StateSet> ss = sgRoot->getOrCreateStateSet();
ss->setMode(GL_LIGHT1, osg::StateAttribute::ON); // (2)
// Create a viewer, use it to view the model
osgViewer::Viewer viewer;
viewer.setSceneData(sgRoot);
// Enter rendering loop
viewer.run();
}
代码中最有趣的部分用 (1)做了标记。 这是我们实例化 osg::LightSource
并使用其 getLight()
方法访问与其关联的 osg::Light
的地方。 然后我们将灯光编号设置为 1,将其位置设置为零(回想一下,这意味着“从 osg::LightSource
的位置开始为零”)并将其漫反射颜色设置为黄色。标有 (2) 的行也有些有趣: 这是我们在整个场景图中启用灯光编号 1 (GL_LIGHT1) 的点。
你有没有想过为什么到目前为止的例子都点亮了,即使我们没有配置任何灯? 这是因为 osgViewer::Viewer
默认为我们配置并启用光源 (GL_LIGHT0)。 在代码中,我使用了 GL_LIGHT1,因为我想保持“默认 OSG 光”正常地将光投射到场景中。 如果我使用零号光,我的灯光设置将覆盖 OSG 默认灯光,导致场景仅由黄色灯光照亮。 (试试吧!)
请注意,核心 OSG 类将只允许使用传统的 OpenGL 照明模型,它以多种方式简化了现实世界的照明,我不会在这里讨论。 OSG 支持更多与光相关的效果(例如阴影,通过 osgShadow nodekit——这又超出了本指南的范围)。
本节结束语:灯光是 OSG 中位置状态最常见的情况,但它们不是唯一的。 裁剪平面是另一种不太罕见的情况,在 OSG 中,它们由两个类处理,osg::ClipNode
和osg::ClipPlane
,它们分别对应于osg::LightSource
和osg::Light
)。
2、纹理的DIY处理
在对纹理做任何其他陈述之前,我必须明确一点,使用 osgDB::readNodeFromFile()
加载纹理模型时,一切都是自动处理的。 我们之前的示例,即使是最简单的示例,也已经能够打开和显示带有纹理的 3D 模型。 我们在这里看到的是你需要在较低级别处理事情的情况。
纹理就是使用纹理坐标在某处查找颜色数据。 通常,这个“某处”是 2D 图像,但也有其他选择,如 1D 纹理、3D 纹理和立方体贴图(顺便说一句,OSG 支持所有这些)。 所以,纹理实际上涉及两个问题。 首先,我们需要一种方法来定义对象上每个顶点的纹理坐标; 其次,我们必须将纹理数据(如图像)绑定到它们。
我不会详细介绍第一个问题,但这里有一些快速提示。 正如我之前所说,如果你从文件加载 3D 模型并且它们已经具有纹理坐标,则这些由 osgDB::readNodeFromFile()
加载。 对于需要以编程方式创建几何图形的情况,你很可能最终会使用 osg::Geometry
类。 这是 osg::Drawable
的一个子类,它允许你从顶点、颜色、法线以及在此上下文中重要的纹理坐标的数组中创建几何体。 详细信息请查阅 osg::Geometry::setTexCoordArray()
。
现在我们可以谈谈第二个问题:将纹理数据绑定到一个对象。 除了我很快就会谈到的一些细节之外,这与我们在上一章中所做的一样:改变我们想要影响的场景图部分的 osg::StateSet
。 对于纹理,我们感兴趣的 osg::StateAttributes
是 osg::Texture
的子类。
我在上一段中提到的小细节是多纹理(将多个纹理绑定到单个对象的能力)。 现代硬件(实际上,甚至不是那么现代的硬件)有多个纹理单元,因此可以非常有效地处理多纹理。 但这带来了以下问题:当我们在 osg::StateSet
中设置纹理属性时,纹理数据将绑定到哪个纹理单元?
这就是为什么我们最好不要使用像 osg::StateSet::setAttributeAndModes()
这样的常规调用来启用纹理属性。 相反,我们应该使用 osg::StateSet::setTextureAttributeAndModes()
,它需要一个额外的参数:一个整数,告诉我们要使用哪个纹理单元。 使用我们的老朋友 osg::StateSet::setAttributeAndModes()
进行纹理化并没有完全失败:OSG 使用第一个纹理单元(单元零),并发出警告告诉你正在做一些不够优雅的操作,并且建议你使用 osg::StateSet::setTextureAttributeAndModes()
, 或者类似的东西。
下面举个例子。 如果你认为我会重复并创建另一个 3D 查看器……那么,你是对的。 这一次,我们的查看器将在显示的对象中应用图像作为纹理。 这个例子的场景图比较枯燥,但是听说插图可以让读者保持清醒,所以如下图所示:
// TexturingViewer.cpp
#include <iostream>
#include <osg/Texture2D>
#include <osg/TexGen>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
int main(int argc, char* argv[])
{
// Check command-line parameters
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <model file>\n";
exit(1);
}
// Load the model
osg::ref_ptr<osg::Node> loadedModel = osgDB::readNodeFile(argv[1]);
if (!loadedModel)
{
std::cerr << "Problem opening '" << argv[1] << "'\n";
exit(1);
}
// Do the texturing stuff
osg::ref_ptr<osg::StateSet> ss = loadedModel->getOrCreateStateSet();
osg::ref_ptr<osg::Image> image = osgDB::readImageFile("texture.png"); // (1)
osg::ref_ptr<osg::Texture2D> tex(new osg::Texture2D()); // (1)
tex->setImage(image); // (1)
ss->setTextureAttributeAndModes(0, tex); // (1)
osg::ref_ptr<osg::TexGen> texGen(new osg::TexGen()); // (2)
texGen->setPlane(osg::TexGen::S, osg::Plane(0.075, 0.0, 0.0, 0.5)); // (2)
texGen->setPlane(osg::TexGen::T, osg::Plane(0.0, 0.035, 0.0, 0.3)); // (2)
ss->setTextureAttributeAndModes(0, texGen); // (2)
// Create a viewer, use it to view the model
osgViewer::Viewer viewer;
viewer.setSceneData(loadedModel);
// Enter rendering loop
viewer.run();
}
此示例中最重要的部分标有 (1)。 它展示了如何使用 osg::Texture2D
将 2D 图像绑定到对象(通过其 osg::StateSet
)。 请注意我们如何使用 setTextureAttributeAndModes()
而不是 setAttributeAndModes()
,在这种特殊情况下,我们使用第一个纹理单元——这就是作为第一个参数传递的零的含义。 这些行中还有其他一些有趣的东西(例如,如何从文件中加载图像),但我认为它们不需要进一步解释。
标有 (2) 的部分也值得多说几句。 在那里,我们使用 osg::TexGen
为我们的对象自动生成纹理坐标。 正如我之前所说,在实践中(至少根据我的特殊经验),我们通常直接从 3D 模型文件中读取纹理坐标,或者我们在 osg::Geometry
中显式设置它们。 然而,在这里,我希望该示例甚至适用于不提供纹理坐标的 3D 模型,所以我选择了 osg::TexGen
。 我不会详细说明它是如何工作的; 我只想说它是 osg::StateAttribute
的另一个子类——封装了OpenGL 的 glTexGen()
调用的子类。 再次注意,我们使用 osg::StateSet::setTextureAttributeAndModes()
传递与之前相同的纹理单元(即纹理单元零)。
你可能想尝试使用此查看器打开一个已经纹理化的 3D 模型。 怎么了? 你能解释为什么吗? 然后,为了尝试多纹理,尝试更改示例程序以使用其他纹理单元(例如,“1”而不是“0”)并再次打开纹理模型。 现在发生了什么? 为什么? 花一些时间思考这些问题并做一些实验肯定会积极地提高你对 OpenGL 和 OSG 中纹理的理解。
3、OSGers 的着色器
作为 OpenGL 之上的一层,OSG 支持用 GLSL(OpenGL 着色器语言)编写的着色器。 OSG 隐藏了在 OpenGL 中使用着色器所需的大部分繁文缛节,但 GLSL 背后的主要概念(program、shader、uniform和 GLSL 语言本身)仍然是正确使用这一日益重要的功能所必需的。 这些主题远远超出了本文的范围,我强烈推荐 OpenGL 着色语言,又名橙皮书,作为它们的指南和参考;Lighthouse 3D 也有一个非常好的 GLSL 教程。
与 OpenGL/GLSL 规范类似,OSG 有两个处理着色器的类: osg::Program
和 osg::Shader
。 此外,第三个类 osg::Uniform
表示统一变量。 osg::Program
是从 osg::StateAtrribute
派生的。 与“纯”OpenGL/GLSL 一样,一个或多个着色器必须附加到一个程序中,这样它才能做一些有用的事情。 uniform一旦被实例化和初始化,就被直接添加到我们的程序将运行的 osg::StateSet
中。
下一个示例说明了如何在 OSG 中使用着色器。 它使用一个包含单个片段着色器的简单程序,该着色器简单地使用统一变量中传递的颜色绘制所有片段。 同样,这个例子的场景图很无聊,因为我们再一次只是在操作显示模型的状态:
// ShaderViewer.cpp
#include <iostream>
#include <osg/Program>
#include <osg/Shader>
#include <osgDB/ReadFile>
#include <osgViewer/Viewer>
const std::string TheShaderSource =
"uniform vec3 rgb;\n"
"void main()\n"
"{\n"
" gl_FragColor = vec4(rgb, 1.0);\n"
"}\n";
int main(int argc, char* argv[])
{
// Check command-line parameters
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <model file>\n";
exit(1);
}
// Load the model
osg::ref_ptr<osg::Node> loadedModel = osgDB::readNodeFile(argv[1]);
if (!loadedModel)
{
std::cerr << "Problem opening '" << argv[1] << "'\n";
exit(1);
}
// Do the shader stuff
osg::ref_ptr<osg::Shader> shader( // (1)
new osg::Shader(osg::Shader::FRAGMENT)); // (1)
shader->setShaderSource(TheShaderSource); // (1)
osg::ref_ptr<osg::Program> program(new osg::Program()); // (2)
program->addShader(shader); // (2)
osg::ref_ptr<osg::StateSet> ss = loadedModel->getOrCreateStateSet();
ss->setAttribute(program);
osg::ref_ptr<osg::Uniform> rgbUniform( // (3)
new osg::Uniform("rgb", osg::Vec3(0.2, 0.2, 1.0))); // (3)
ss->addUniform(rgbUniform); // (3)
// Create a viewer, use it to view the model
osgViewer::Viewer viewer;
viewer.setSceneData(loadedModel);
// Enter rendering loop
viewer.run();
}
在标有 (1) 的行中,我们实例化着色器并设置其源代码。 在这种情况下,我们创建了一个片段着色器( osg::Shader::FRAGMENT
;OSG 还支持 osg::Shader::GL_VERTEX_SHADER
和 osg::Shader::GL_GEOMETRY_SHADER_EXT
),并直接利用setShaderSource()
从字符串设置其源代码,此外还有一个方便的 loadShaderSourceFromFile()
方法可以从文件读取着色器源码。
然后,在 (2) 中,我们创建程序并将着色器添加到其中。 与在不使用 OSG 的情况下使用 GLSL 一样,可以将多个着色器添加到同一个程序中。 该程序被添加到状态集中,就像任何其他 osg::StateAtrribute
一样。 标有 (3) 的行展示了如何创建uniform变量并将其添加到 osg::StateSet
,从而使uniform变量可用于着色器。
这几乎是 OSG 特有的关于着色器的所有知识。 我可以提一下,OSG 自动定义了一些方便的uniform变量,如 osg_FrameNumber、osg_FrameTime、osg_DeltaFrameTime、osg_ViewMatrix 和 osg_ViewMatrixInverse,但这对初学者来说过于冗长了。
原文链接:Open Scene Graph: More State — Lights, Textures and Shaders
BimAnt翻译整理,转载请标明出处