如何向Shader传递上万对象数据?纹理提取与实例化绘制疑问
我希望通过一次请求让Shader绘制屏幕上的所有对象,当前逐个切换Shader绘制的方式速度极慢,该优化预计能提升100+帧率。
我了解过FBO,但需要在FBO写入的纹理中定位对象,因此开始研究如何向Shader传递大量位置及其他信息。我发现可以通过纹理传递,但遇到了困难:我知道需要将f32类型的坐标数组转为字节数组写入纹理,已完成写入操作,但完全不清楚如何在GLSL中提取这些数据,且网上关于GLSL的相关资料极少。
补充说明
所有对象均为相同形状(圆形),我希望先理解核心传递原理,再自行扩展。目前已通过Uniform传递其他数据,但4096的数量限制无法满足当前需求。我看到过使用实例化和纹理的示例,但不理解OpenGL如何识别对象,比如屏幕上有3个红色圆形时,gl_InstanceID是否会遍历所有圆形?我尝试了实例化的方式,但对gl_InstanceID的作用仍有疑惑,现附上相关代码尝试:
Rust代码片段
use macroquad::prelude::*; #[macroquad::main("Texture")] async fn main() { let bytes: Vec<u8> = vec![ 0, 255, 0, 255, 255, 255, 0, 255, 255, 0, 0, 255, 255, 0, 255, 255 ]; let texture = Texture2D::from_rgba8(2, 2, &bytes); // here i write rgba array, but need write bytes of f32 array like [6.20, 50.31231, 44.1213, 994.44] let lens_material = load_material( ShaderSource::Glsl { vertex: VERTEX_SHADER, fragment: FRAGMENT_SHADER, }, MaterialParams { uniforms: vec![ UniformDesc::new("Size", UniformType::Float2), UniformDesc::new("Position", UniformType::Float2) ], textures: vec!["MyTexture".to_string()], ..Default::default() }, ) .unwrap(); let size = vec2(300., 300.); let pos1 = vec2(screen_width()/2. - size.x / 8., screen_height()/2. - size.y / 8.); let mut pos2 = vec2(screen_width()/2. - size.x / 2., screen_height()/2. - size.y / 2.); let positions = vec![pos1, pos2]; let bounds = vec2(14000., 14000.); // Position bounds let normalized_positions: Vec<Vec2> = positions.iter().map(|&p| p / bounds * 255.).collect(); dbg!(normalized_positions); dbg!(screen_width(), screen_height()); loop { clear_background(WHITE); lens_material.set_texture("MyTexture", texture.clone()); lens_material.set_uniform("Size", size); lens_material.set_uniform("Position", pos1 + size / 2.); // Rect gl_use_material(&lens_material); { draw_rectangle(pos1.x, pos1.y, size.x, size.y, WHITE); } gl_use_default_material(); draw_circle(screen_width() / 2., screen_height()/2., 60., PURPLE); lens_material.set_texture("MyTexture", texture.clone()); lens_material.set_uniform("Size", size / 2.); lens_material.set_uniform("Position", pos2); // Circle gl_use_material(&lens_material); { draw_circle(pos2.x, pos2.y, size.x / 2., WHITE); } gl_use_default_material(); next_frame().await; } } const FRAGMENT_SHADER: &'static str = r#" #version 300 es #ifdef GL_ES precision highp float; #endif uniform sampler2D MyTexture; in vec2 uv; out vec4 FragColor; void main() { float dist = length(uv - vec2(0.5, 0.5)); // if (dist < 0.5) { vec4 texColor = texture(MyTexture, uv); FragColor = texColor; // } } "#; const VERTEX_SHADER: &'static str = r#" #version 300 es #ifdef GL_ES precision highp float; #endif uniform vec2 Size; uniform vec2 Position; in vec2 position; uniform mat4 Projection; uniform mat4 Model; out vec2 uv; void main() { gl_Position = Projection * Model * vec4(position, 0.0, 1.); vec2 normalizedPosition = (position - Position) / Size * 0.5 + 0.5; // [0; 1] uv = normalizedPosition; } "#;
补充代码尝试
let size = vec2(300., 300); let pos1 = vec2(screen_width()/2. - size.x / 8., screen_height()/2. - size.y / 8.); let mut pos2 = vec2(screen_width()/2. - size.x / 2., screen_height()/2. - size.y / 2.); let positions: Vec<f32> = vec![ pos1.x, // Pos 1 pos1.y, // Pos 1 0.0, // Pos 1 0.0, // Pos 1 pos2.x, // Pos 2 pos2.y, // Pos 2 0.0, // Pos 2 0.0 // Pos 2 ]; let mut bytes: [u8; 64] = [0u8; 64]; let pos_bytes: &[u8] = bytemuck::cast_slice(&positions); for (i, &byte) in pos_bytes.iter().enumerate() { bytes[i] = byte; } let positions_texture = Texture2D::from_rgba8(4, 4, &bytes); lens_material.set_texture("PositionsTexture", positions_texture.clone());
顶点着色器修改片段
void main() { gl_Position = Projection * Model * vec4(position, 0.0, 1.); vec4 Position = texture(PositionsTexture, vec2(gl_InstanceID, gl_InstanceID)); vec2 normalizedPosition = (position - Position.xy) / Size * 0.5 + 0.5; // [0; 1] uv = normalizedPosition; }
一、通过纹理传递f32数据并在GLSL中提取的正确方法
1. 纹理数据的正确写入
当前用from_rgba8创建纹理是错误的,这个方法针对RGBA8格式设计,存储f32数据会丢失精度。需要使用浮点纹理格式来保存原始f32值:
// 假设存储N个对象的x,y坐标,按顺序排列成f32数组 let positions: Vec<f32> = vec![pos1.x, pos1.y, pos2.x, pos2.y, ...]; // 将f32数组转为字节数组 let pos_bytes = bytemuck::cast_slice(&positions); // 创建浮点纹理:宽度设为每个对象的f32分量数(这里是2),高度设为对象总数,格式用RG32F let positions_texture = Texture2D::from_raw_bytes( 2, // 宽度:每个对象占2个f32(x,y) positions.len() / 2, // 高度:对象总数 pos_bytes, TextureFormat::RG32F, // 每个像素存储两个f32值,无精度损失 );
如果需要存储更多属性(比如半径、颜色),可以改用RGBA32F格式,每个像素存储4个f32值。
2. GLSL中提取f32数据的正确方式
纹理采样需要用归一化坐标(范围[0,1]),不能直接用gl_InstanceID作为坐标。修改后的顶点着色器逻辑如下:
#version 300 es precision highp float; uniform sampler2D PositionsTexture; uniform vec2 ObjectSize; // 所有对象的统一大小 uniform mat4 Projection; uniform mat4 Model; in vec2 position; out vec2 uv; void main() { // 获取纹理高度(即对象总数) int textureHeight = textureSize(PositionsTexture, 0).y; // 计算当前实例对应的纹理行的归一化y坐标,+0.5是为了采样像素中心 float texY = (float(gl_InstanceID) + 0.5) / float(textureHeight); // 纹理x坐标固定为0.5,因为每个行只存一个对象的x,y vec2 texCoord = vec2(0.5, texY); // 采样纹理,得到当前实例的位置 vec2 instancePos = texture(PositionsTexture, texCoord).xy; // 计算顶点最终位置:基础顶点(单位矩形)缩放后加上实例偏移 vec2 vertexPos = position * ObjectSize + instancePos; gl_Position = Projection * Model * vec4(vertexPos, 0.0, 1.0); // 生成UV坐标 uv = position * 0.5 + 0.5; }
二、gl_InstanceID的作用机制
gl_InstanceID是OpenGL实例化绘制时自动分配的实例索引,核心逻辑:
- 触发条件:调用
glDrawArraysInstanced或glDrawElementsInstanced(Macroquad中对应draw_mesh_instanced)时,OpenGL会重复绘制指定图元N次,N为实例数。 - 取值规则:从0开始递增,第0个实例对应
gl_InstanceID=0,第1个实例对应gl_InstanceID=1,直到第N-1个实例对应gl_InstanceID=N-1。 - 场景示例:如果要绘制3个圆形,调用实例化绘制时指定实例数为3,
gl_InstanceID会依次取0、1、2,你可以用这个索引从纹理中获取对应圆形的位置、大小等数据。
实例化绘制的正确流程(Macroquad)
// 1. 创建基础网格(单位矩形,用于承载圆形Shader) let mesh = Mesh::from_data(MeshData { positions: vec![ -0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.5, 0.5, 0.0, -0.5, 0.5, 0.0, ], uvs: vec![ 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, ], indices: vec![0, 1, 2, 0, 2, 3], ..Default::default() }); // 2. 渲染循环中调用实例化绘制 loop { clear_background(WHITE); gl_use_material(&lens_material); // 绘制N个实例,N为positions数组中的对象总数 draw_mesh_instanced(&mesh, positions.len() / 2); gl_use_default_material(); next_frame().await; }
关键错误修正
你之前直接用vec2(gl_InstanceID, gl_InstanceID)作为纹理坐标是错误的,因为纹理坐标需要归一化到[0,1]范围,而gl_InstanceID是整数索引,远大于1,会导致采样到纹理的重复区域,无法正确获取数据。
内容的提问来源于stack exchange,提问作者Someone

