Shader开发(十七)着色器中的纹理采样与渲染

纹理映射作为现代三维图形渲染的核心技术之一,其实现机制涉及CPU与GPU之间的复杂数据传输过程。在建立了完善的UV坐标系统基础后,本节将深入探讨纹理数据在着色器管线中的加载、传输和采样技术,详细阐述从图像文件到屏幕像素的完整渲染流程。


加载图像并准备纹理

在OpenFrameworks框架中,纹理映射系统需要两个核心组件:图像数据载体和纹理对象。为实现高效的纹理管理,需要在应用程序类中声明相应的成员变量。

纹理映射所需的类成员变量声明:

class ofApp : public ofBaseApp {
public:
    // 核心渲染组件
    ofMesh quad;           // 几何网格对象
    ofShader shader;       // 着色器程序
    ofImage img;           // 图像数据容器
};

纹理数据的初始化过程包含两个关键步骤:系统兼容性配置和图像文件加载。OpenFrameworks框架为保持向后兼容性,默认采用非标准化的纹理坐标系统,因此需要显式禁用此传统功能以确保与现代图形API的兼容性。

setup() 函数中,加载图像并禁用旧式纹理坐标系统(ARB 纹理),以采用标准 UV 坐标:

void ofApp::setup()
{
    // 禁用传统纹理坐标系统,启用标准UV坐标
    ofDisableArbTex();
    
    // 从应用程序数据目录加载纹理图像
    img.load("parrot.png");
    
    // 其他初始化代码...
}

ofDisableArbTex()函数的调用确保了纹理坐标使用标准化的[0,1]范围,而非像素坐标系统。图像文件路径采用相对于bin/data目录的相对路径规范,这种设计简化了资源管理并提高了项目的可移植性。


设置统一纹理变量

纹理数据从CPU内存传输到GPU显存的过程通过统一变量机制实现。由于纹理数据在单次渲染调用中保持不变,而仅UV坐标根据片元位置动态变化,因此将纹理作为统一变量进行处理是最优的架构选择。

draw() 函数中,使用 setUniformTexture() 将图像转换为纹理并绑定:

void ofApp::draw()
{
    shader.begin();                                    // 激活着色器程序
    shader.setUniformTexture("parrotTex", img, 0);     // 传输纹理到GPU
    quad.draw();                                       // 执行几何体渲染
    shader.end();                                      // 释放着色器资源
}

参数说明:

  • "parrotTex":统一变量名称。

  • img:图像对象。

  • 0:纹理位置(单一纹理时默认值;多纹理需唯一)。

更新 setup() 以加载新着色器文件。

setUniformTexture()函数接受三个关键参数,其设计体现了现代GPU架构的纹理单元管理机制:

  1. 统一变量名称:着色器中纹理采样器的标识符

  2. 图像对象引用:CPU端的纹理数据源

  3. 纹理单元索引:GPU端的纹理槽位标识

纹理单元索引在多纹理渲染场景中至关重要。GPU通常提供多个纹理单元(现代GPU通常支持16个或更多),每个纹理必须绑定到不同的单元以避免资源冲突。对于单纹理应用,索引值0即可满足需求。


片元着色器:纹理采样

片元着色器使用 sampler2D 类型声明纹理统一变量,并通过 texture() 函数采样:

#version 410

uniform sampler2D parrotTex;        // 二维纹理采样器

in vec2 fragUV;                     // 插值后的UV坐标
out vec4 outCol;                    // 最终颜色输出

void main()
{
    // 执行纹理采样并输出颜色
    outCol = texture(parrotTex, fragUV);
}

sampler2D 为常见 2D 纹理采样器;texture() 返回指定 UV 的颜色值。

texture()函数是GLSL内建的核心采样函数,其执行机制涉及复杂的硬件加速过程:

  1. 坐标验证:验证UV坐标的有效性范围

  2. 像素定位:根据UV坐标计算纹理像素位置

  3. 插值计算:执行双线性或其他插值算法

  4. 颜色返回:返回RGBA格式的颜色值

该函数的硬件实现确保了纹理采样的高效性能,使得实时渲染中的大规模纹理采样成为可能。


处理纹理方向问题

在纹理映射实现过程中,经常遇到图像垂直翻转的技术问题。此问题源于OpenGL坐标系统与标准图像文件格式之间的根本差异:

  • OpenGL规范:纹理坐标原点位于左下角,Y轴向上递增

  • 图像文件格式:像素数据从顶部开始存储,Y轴向下递增

针对此兼容性问题,最高效的解决方案是在顶点着色器中执行实时坐标转换,避免CPU端的图像预处理开销。

void main()
{
    gl_Position = vec4(pos, 1.0);
    
    // 不翻转
    // fragUV = uv;

    // 执行Y坐标翻转:将[0,1]范围内的Y坐标反转
    fragUV = vec2(uv.x, 1.0 - uv.y);
}

此坐标转换利用了UV坐标标准化范围[0,1]的特性:通过1.0 - uv.y操作,实现了Y坐标的完全翻转,使得原本位于底部的纹理区域映射到顶部,反之亦然。


渲染结果

运行后,应显示完整纹理图像。注意,纹理翻转需求视图形 API 而异,本文基于 OpenGL。


总结

纹理映射技术的成功实现需要协调CPU端的资源管理、GPU端的数据传输以及着色器中的采样计算等多个技术层面。通过统一变量机制实现的纹理传输,结合GLSL内建的高效采样函数,为复杂的视觉效果提供了技术基础。


项目代码参考

ofApp.h

#pragma once

#include "ofMain.h"

class ofApp : public ofBaseApp {

public:
    void setup();
    void update();
    void draw();

    void keyPressed(int key);
    void keyReleased(int key);
    void mouseMoved(int x, int y);
    void mouseDragged(int x, int y, int button);
    void mousePressed(int x, int y, int button);
    void mouseReleased(int x, int y, int button);
    void mouseEntered(int x, int y);
    void mouseExited(int x, int y);
    void windowResized(int w, int h);
    void dragEvent(ofDragInfo dragInfo);
    void gotMessage(ofMessage msg);

    ofMesh quad;
    ofShader shader;
    ofImage img;
};

ofApp.cpp

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup()
{
    quad.addVertex(glm::vec3(-1, -1, 0));
    quad.addVertex(glm::vec3(-1, 1, 0));
    quad.addVertex(glm::vec3(1, 1, 0));
    quad.addVertex(glm::vec3(1, -1, 0));

    quad.addColor(ofDefaultColorType(1, 0, 0, 1)); //red
    quad.addColor(ofDefaultColorType(0, 1, 0, 1)); //green
    quad.addColor(ofDefaultColorType(0, 0, 1, 1)); //blue
    quad.addColor(ofDefaultColorType(1, 1, 1, 1)); //white

    quad.addTexCoord(glm::vec2(0, 1));
    quad.addTexCoord(glm::vec2(0, 0));
    quad.addTexCoord(glm::vec2(1, 0));
    quad.addTexCoord(glm::vec2(1, 1));

    ofIndexType indices[6] = { 0,1,2,2,3,0 };
    quad.addIndices(indices, 6);

    shader.load("passthrough.vert", "texture.frag");
    
    // 禁用传统纹理坐标系统,启用标准UV坐标
    ofDisableArbTex();
    // 从应用程序数据目录加载纹理图像
    img.load("parrot.png");
}


//--------------------------------------------------------------
void ofApp::update() {

}

//--------------------------------------------------------------
void ofApp::draw() {
    shader.begin();                                    // 激活着色器程序
    shader.setUniformTexture("parrotTex", img, 0);     // 传输纹理到GPU
    quad.draw();                                       // 执行几何体渲染
    shader.end();                                      // 释放着色器资源
}

//--------------------------------------------------------------
void ofApp::keyPressed(int key) {

}

//--------------------------------------------------------------
void ofApp::keyReleased(int key) {

}

//--------------------------------------------------------------
void ofApp::mouseMoved(int x, int y) {

}

//--------------------------------------------------------------
void ofApp::mouseDragged(int x, int y, int button) {

}

//--------------------------------------------------------------
void ofApp::mousePressed(int x, int y, int button) {

}

//--------------------------------------------------------------
void ofApp::mouseReleased(int x, int y, int button) {

}

//--------------------------------------------------------------
void ofApp::mouseEntered(int x, int y) {

}

//--------------------------------------------------------------
void ofApp::mouseExited(int x, int y) {

}

//--------------------------------------------------------------
void ofApp::windowResized(int w, int h) {

}

//--------------------------------------------------------------
void ofApp::gotMessage(ofMessage msg) {

}

//--------------------------------------------------------------
void ofApp::dragEvent(ofDragInfo dragInfo) {

}

main.cpp

#include "ofMain.h"
#include "ofApp.h"

//========================================================================
int main() {
    ofGLWindowSettings glSettings;
    glSettings.setSize(1024, 768); //was 748 vertical
    glSettings.windowMode = OF_WINDOW;
    glSettings.setGLVersion(4, 1);
    ofCreateWindow(glSettings);

    printf("%s\n", glGetString(GL_VERSION));
    ofRunApp(new ofApp());

}

passthrough.vert

#version 410

layout (location = 0) in vec3 pos; 
layout (location = 3) in vec2 uv;

out vec2 fragUV;

void main()
{
	gl_Position = vec4(pos, 1.0);
	fragUV = uv;
    // 执行Y坐标翻转:将[0,1]范围内的Y坐标反转
    // fragUV = vec2(uv.x, 1.0 - uv.y);
}

texture.frag

#version 410

uniform sampler2D parrotTex;

in vec2 fragUV;
out vec4 outCol;

void main()
{
	outCol = texture(parrotTex, fragUV);
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值