You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何向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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 20:25:54