Shader开发(十八)实现纹理滚动效果
在图形渲染中,通过操纵 UV 坐标实现动态效果是常见技术。本章节探讨如何调整顶点着色器中的 UV 坐标,以创建纹理滚动的视觉效果,类似于跑马灯。通过设置纹理的 wrap mode 和引入时间统一变量,实现纹理的连续滚动。
操纵 UV 坐标实现滚动
在传统的纹理映射中,四边形的UV坐标与纹理的标准坐标完全对应,均限制在[0,1]的标准化范围内。然而,着色器系统支持对超出此范围的坐标进行采样操作。
当UV坐标超出标准范围时,最终的视觉效果取决于纹理的Wrap Mode配置。
为了演示UV坐标偏移的基本原理,首先创建一个基础的顶点着色器实现。
#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 = vec2(uv.x, 1.0 - uv.y) + vec2(0.25, 0.0); // UV坐标偏移
}
通过向UV坐标添加固定偏移向量vec2(0.25, 0.0)
,四边形的纹理坐标范围从原始的[0,1]扩展至[0.25,1.25]。这种坐标扩展使得屏幕左侧顶点的X分量UV坐标达到0.25以上,而右侧顶点的X分量则达到1.25,超出了标准纹理坐标范围。
图1 加上偏移UV坐标的鹦鹉纹理
设置纹理 Wrap Mode
在默认配置下,GPU采用截取(Clamp)模式处理超出[0,1]范围的UV坐标。具体而言,任何大于1.0的坐标值都被强制截取为1.0,小于0.0的值则被截取为0.0。这种处理方式导致纹理边缘像素在超出范围的区域内重复显示,产生图1所示的视觉效果。
对于动态滚动效果的实现需求,Clamp模式显然无法满足要求,因为它无法实现纹理的连续循环显示。
为实现理想的滚动效果,需要将纹理环绕模式配置为Repeat(重复)模式。在此模式下,坐标值1.25会被自动转换为0.25,从而实现纹理的无缝循环。
在 setup()
函数中设置:
void ofApp::setup()
{// 其他初始化代码省略img.load("parrot.png");img.getTexture().setTextureWrap(GL_REPEAT, GL_REPEAT); // 设置重复模式
}
setTextureWrap()
函数接受两个参数,分别用于配置水平和垂直方向的环绕模式。在本实现中,两个方向均设置为GL_REPEAT
模式。需要注意的是,此处首次需要通过getTexture()
方法获取底层的ofTexture
对象,以便直接操作纹理参数。
图2 全屏四边形上使用"REPEAT"wrap mode的鹦鹉纹理
引入时间统一变量实现动画
着色器本身不具备时间感知能力,因此需要通过统一变量机制从CPU端传递时间信息。由于时间值在单次渲染过程中保持恒定,使用统一变量是最适合的数据传输方式。
为使滚动随时间变化,在顶点着色器中添加时间统一变量:
#version 410
layout(location = 0) in vec3 pos;
layout(location = 3) in vec2 uv;uniform float time; // 时间统一变量
out vec2 fragUV;void main()
{gl_Position = vec4(pos, 1.0);fragUV = vec2(uv.x, 1.0 - uv.y) + vec2(1.0, 0.0) * time; // 时间驱动的偏移
}
在时间驱动的实现中,偏移向量被设计为vec2(1.0, 0.0) * time
。这里使用单位向量(1.0, 0.0)
作为基础方向向量,通过与时间值相乘来控制偏移幅度。
当应用程序运行t秒时,UV坐标的偏移量为(t, 0)
,实现了线性的水平滚动效果。这种设计确保了动画的连续性和可预测性。
完整的时间驱动系统还需要CPU端的配合,通过统一变量将当前时间传递给着色器。
在 draw()
函数中传递时间值:
void ofApp::draw() {shader.begin();shader.setUniformTexture("parrot", img, 0);shader.setUniform1f("time", ofGetElapsedTimef()); // 传递运行时间quad.draw();shader.end();
}
ofGetElapsedTimef()
函数返回自程序启动以来经过的秒数,为着色器提供了连续的时间基准。通过setUniform1f()
方法,这个时间值被传递给着色器的time
统一变量。
完成上述技术实现后,程序将展现出纹理沿水平方向连续滚动的动态效果。由于Repeat环绕模式的配置,纹理在滚动过程中实现了无缝循环,创造出类似跑马灯的视觉效果。
小结
本文演示了 UV 坐标滚动的技术,包括偏移、wrap mode 配置及时间动画。UV坐标的动态操控技术为着色器开发提供了强大的创意表达工具。通过对纹理坐标的数学变换,开发者可以在保持几何复杂度不变的前提下,创造出丰富多样的视觉效果。
项目代码参考
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)); //redquad.addColor(ofDefaultColorType(0, 1, 0, 1)); //greenquad.addColor(ofDefaultColorType(0, 0, 1, 1)); //bluequad.addColor(ofDefaultColorType(1, 1, 1, 1)); //whitequad.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("scrolling_uv.vert", "texture.frag");ofDisableArbTex();img.load("parrot.png");img.getTexture().setTextureWrap(GL_REPEAT, GL_REPEAT); // 设置重复模式
}//--------------------------------------------------------------
void ofApp::update() {}//--------------------------------------------------------------
void ofApp::draw() {shader.begin();shader.setUniformTexture("parrot", img, 0);shader.setUniform1f("time", ofGetElapsedTimef()); // 传递运行时间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 verticalglSettings.windowMode = OF_WINDOW;glSettings.setGLVersion(4, 1);ofCreateWindow(glSettings);printf("%s\n", glGetString(GL_VERSION));ofRunApp(new ofApp());}
scrolling_uv.vert
#version 410layout (location = 0) in vec3 pos;
layout (location = 3) in vec2 uv;uniform float time;
out vec2 fragUV;void main()
{gl_Position = vec4(pos, 1.0);
// fragUV = uv + vec2(0.25, 0.0);// 执行Y坐标翻转:将[0,1]范围内的Y坐标反转// fragUV = vec2(uv.x, 1.0 - uv.y);fragUV = uv + vec2(1.0,0.0)*time;
}
texture.frag
#version 410uniform sampler2D parrot;in vec2 fragUV;
out vec4 outCol;void main()
{outCol = texture(parrot, fragUV);
}