使用OpenGL ES 2.0渲染多个对象

33
我正在学习OpenGL ES 2.0以进行iPhone游戏开发。我已经阅读了多个教程和部分OpenGL ES 2.0规范。所有我看到的例子都创建了一个单一的网格,将其加载到顶点缓冲区中,然后渲染它(带有预期的平移、旋转、梯度等)。
我要问的是:如何在场景中渲染具有不同网格并独立移动的多个对象?例如,如果我有一辆汽车和一辆摩托车,我可以创建2个顶点缓冲区,并为每个渲染调用保留两者的网格数据,然后只需为每个对象发送不同的着色器矩阵即可吗?还是我需要以某种方式平移这些网格,然后将它们组合成单个网格,以便可以在一次渲染中呈现它们?我正在寻找更高层次的策略/程序结构,而不是代码示例。我想我对它的运作方式有误解。
谢谢!

5
这是一个非常好的问题。 - Bedir Yilmaz
大多数示例只旋转和平移一个对象视图一次。要在不同角度和位置绘制两个对象,请在第一个对象之后应用相反的操作。例如:translate1、rotate1、draw1、-rotate1、-translate1、translate2、rotate2、draw2、-rotate2、-translate2。我很快会发布一个示例。 - Gary Davies
6个回答

13
我发现最好的方法是除了使用VBOs之外,还要使用VAOs。
首先,我会仅使用VBOs回答您的问题。
首先,假设您已将两个对象的两个网格存储在以下数组中:
GLuint _vertexBufferCube1;
GLuint _vertexBufferCube2;

在哪里:

GLfloat gCubeVertexData1[36] = {...};
GLfloat gCubeVertexData2[36] = {...};

你还需要顶点缓冲区:

GLuint _vertexBufferCube1;
GLuint _vertexBufferCube2;

现在,要绘制这两个立方体(无需使用VAO),您需要执行以下操作: 在draw函数中(从OpenGLES模板中):
//Draw first object, bind VBO, adjust your attributes then call DrawArrays
glGenBuffers(1, &_vertexBufferCube1);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferCube1);
glBufferData(GL_ARRAY_BUFFER, sizeof(gCubeVertexData1), gCubeVertexData1, GL_STATIC_DRAW);

glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribNormal);
glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(12));


glDrawArrays(GL_TRIANGLES, 0, 36);



//Repeat for second object:
glGenBuffers(1, &_vertexBufferCube2);
glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferCube2);
glBufferData(GL_ARRAY_BUFFER, sizeof(gCubeVertexData2), gCubeVertexData2, GL_STATIC_DRAW);

glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribNormal);
glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(12));
glUseProgram(_program);

glDrawArrays(GL_TRIANGLES, 0, 36);

这将回答您的问题。但是,现在使用VAOs,您的绘制函数代码要简单得多(这很好,因为它是重复的函数):
首先,您需要定义两个VAOs:
GLuint _vertexArray1;
GLuint _vertexArray2;

然后您将执行draw方法中执行的所有步骤,但是在绑定到VAO之后,您将在setupGL函数中执行它。然后在您的draw函数中,您只需绑定到所需的VAO即可。

这里的VAO类似于包含许多属性的配置文件(想象一下智能设备配置文件)。与每次更改颜色、桌面、字体等不同,您只需更改一次并将其保存在一个配置文件名称下。然后您只需要切换配置文件。

所以您只需在setupGL内部进行一次操作,然后在draw中进行切换。

当然,您可以将代码(不包括VAO)放入函数中并调用它。这是正确的,但是根据Apple的说法,VAO更有效率:

http://developer.apple.com/library/ios/#documentation/3DDrawing/Conceptual/OpenGLES_ProgrammingGuide/TechniquesforWorkingwithVertexData/TechniquesforWorkingwithVertexData.html#//apple_ref/doc/uid/TP40008793-CH107-SW1

现在到代码部分:
在setupGL函数中:
glGenVertexArraysOES(1, &_vertexArray1); //Bind to first VAO
glBindVertexArrayOES(_vertexArray1);

glGenBuffers(1, &_vertexBufferCube1); //All steps from this one are done to first VAO only
glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferCube1);
glBufferData(GL_ARRAY_BUFFER, sizeof(gCubeVertexData1), gCubeVertexData1, GL_STATIC_DRAW);

glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribNormal);
glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(12));

glGenVertexArraysOES(1, &_vertexArray2); // now bind to the second
glBindVertexArrayOES(_vertexArray2);

glGenBuffers(1, &_vertexBufferCube2); //repeat with the second mesh
glBindBuffer(GL_ARRAY_BUFFER, _vertexBufferCube2);
glBufferData(GL_ARRAY_BUFFER, sizeof(gCubeVertexData2), gCubeVertexData2, GL_STATIC_DRAW);

glEnableVertexAttribArray(GLKVertexAttribPosition);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(0));
glEnableVertexAttribArray(GLKVertexAttribNormal);
glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 24, BUFFER_OFFSET(12));


glBindVertexArrayOES(0);

最后,在您的绘制方法中:
glBindVertexArrayOES(_vertexArray1);
glDrawArrays(GL_TRIANGLES, 0, 36);


glBindVertexArrayOES(_vertexArray2);    
glDrawArrays(GL_TRIANGLES, 0, 36);

谢谢,使用VAO是一个很好的方式。 - Ahmed Kotb
1
如果我需要创建100个对象,那么我需要100个VAO吗? - MatterGoal
请注意,VAO 在所有 ES 2.0 设备上都不能保证可用。(如果是 ES 3.0 则可以保证)如果您正在编写 2.0 版本的程序,则调用 glGetString(GL_EXTENSIONS),并在响应中搜索字符串 OES_vertex_array_object。https://www.khronos.org/opengles/sdk/1.1/docs/man/glGetString.xml 特别是在 Android 上,由于设备种类繁多,因此您可以通知用户您的应用无法运行,或者退回到仅使用 VBO 的方法。 - ToolmakerSteve

9
您需要为不同的对象维护单独的顶点/索引缓冲区。例如,您可能会有一个RenderedObject类,每个实例都有自己的顶点缓冲区。一个RenderedObject可能会从一个房屋网格中获取其顶点,而另一个RenderedObject则可能来自一个角色网格等。
在渲染期间,您为正在使用的顶点缓冲区设置适当的变换/旋转/着色,例如:
void RenderedObject::render()
{
    ...
    //set textures/shaders/transformations

    glBindBuffer(GL_ARRAY_BUFFER, bufferID);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, vertexCount);
    ...
}

如其他回答所提到的,bufferID只是一个GLuint,而不是整个缓冲区的内容。如果您需要更多有关创建顶点缓冲区并填充数据的详细信息,我很乐意补充。


2
我的问题已经延迟了2年,但是:如果对于不同的对象有不同的顶点缓冲区,那么您还必须将程序绑定到所有这些对象。在我的情况下,您很容易得到超过1000个对象,您必须每次都调用setVertexAttribPointer。在我的情况下,这导致它变得非常缓慢(事实上,“setVertexAttribPointer”的时间占了60-70% <-您是否知道这一点或者我做错了什么? - Frame91
1
我的理解是这可能效率低下。例如:如果你想画10k只鸟,那么这将非常慢。我的理解(我不是专家)是你应该尝试合并相似的对象,或者所有对象,以最小化绘制调用的数量。不幸的是,我不确定如何实现这一点 - 尽管理论上是完全有道理的! - AlvinfromDiaspar
1
是的,如果切换顶点/索引缓冲区的频率过高,成本会变得非常昂贵。在10K鸟的情况下,很有可能只有少数几个不同的模型用于表示这些鸟。您需要为每种模型类型设置一个顶点缓冲区,并为每个实例使用不同的转换重新绘制缓冲区。因此,设置Sparrow顶点缓冲区,绘制2K只麻雀,然后设置Falcon VB并绘制3K只猎鹰,依此类推。 - TaylorP

4
我知道这是一篇较旧的帖子,但我正在寻找如何在 OpenGL 中呈现多个对象的说明。我发现了一个很棒的教程,它描述了如何呈现多个对象,并且可以轻松扩展以呈现不同类型的对象(例如一个立方体和一个金字塔)。
我发布的教程还介绍了如何使用 GLKit 来呈现对象。我觉得这很有帮助,所以在此转发。希望对你有所帮助! http://games.ianterrell.com/opengl-basics-with-glkit-in-ios5-encapsulated-drawing-and-animation/

链接已损坏 - 新的URL? - ToolmakerSteve
看起来教程的网站名称可能没有续订。但是,教程中的代码在GitHub上构建:https://github.com/ianterrell/GLKit-Cube-Tutorial 这是我能找到的最好的。看起来他的包含教程的网站现在已经下线了,看起来是永久性的。希望他能将教程内容迁移到新的地方。:-) - MikeyE

3

如果网格不同,您将它们保存在不同的顶点缓冲区中。如果它们相似(例如动画、颜色),则向着色器传递参数。如果您不打算在应用程序端对对象进行动画处理,则只需保留VBO的句柄,而不是顶点数据本身。设备端动画是可能的。


0

使用着色器时,可以使用相同的程序来处理所有对象,而无需为每个对象编译、连接和创建一个程序。要实现这一点,只需将GLuint值存储到程序中,然后对于每个对象使用"glUseProgram(programId);"即可。基于个人经验,我使用单例来管理GLProgram结构(在下面提供:)

@interface TDShaderSet : NSObject {

    NSMutableDictionary     *_attributes;
    NSMutableDictionary     *_uniforms;
    GLuint                  _program;

}

    @property (nonatomic, readonly, getter=getUniforms) NSMutableDictionary *uniforms;
    @property (nonatomic, readonly, getter=getAttributes) NSMutableDictionary *attributes;

    @property (nonatomic, readonly, getter=getProgram) GLuint program;

    - (GLint) uniformLocation:(NSString*)name;
    - (GLint) attribLocation:(NSString*)name;

@end


@interface TDProgamManager : NSObject

    + (TDProgamManager *) sharedInstance;
    + (TDProgamManager *) sharedInstanceWithContext:(EAGLContext*)context;

    @property (nonatomic, readonly, getter=getAllPrograms) NSArray *allPrograms;

    - (BOOL) loadShader:(NSString*)shaderName referenceName:(NSString*)refName;

    - (TDShaderSet*) getProgramForRef:(NSString*)refName;

@end

@interface TDProgamManager () {

    NSMutableDictionary     *_glPrograms;
    EAGLContext             *_context;

}

@end


@implementation TDShaderSet

    - (GLuint) getProgram
    {
        return _program;
    }

    - (NSMutableDictionary*) getUniforms
    {
        return _uniforms;
    }

    - (NSMutableDictionary*) getAttributes
    {
        return _attributes;
    }

    - (GLint) uniformLocation:(NSString*)name
    {
        NSNumber *number = [_uniforms objectForKey:name];
        if (!number) {
            GLint location = glGetUniformLocation(_program, name.UTF8String);
            number = [NSNumber numberWithInt:location];
            [_uniforms setObject:number forKey:name];
        }
        return number.intValue;
    }

    - (GLint) attribLocation:(NSString*)name
    {
        NSNumber *number = [_attributes objectForKey:name];
        if (!number) {
            GLint location = glGetAttribLocation(_program, name.UTF8String);
            number = [NSNumber numberWithInt:location];
            [_attributes setObject:number forKey:name];
        }
        return number.intValue;
    }

    - (id) initWithProgramId:(GLuint)program
    {
        self = [super init];
        if (self) {
            _attributes = [[NSMutableDictionary alloc] init];
            _uniforms = [[NSMutableDictionary alloc] init];
            _program = program;
        }
        return self;
    }

@end


@implementation TDProgamManager {

@private

}

    static TDProgamManager *_sharedSingleton = nil;

    - (NSArray *) getAllPrograms
    {
        return _glPrograms.allValues;
    }

    - (TDShaderSet*) getProgramForRef:(NSString *)refName
    {
        return (TDShaderSet*)[_glPrograms objectForKey:refName];
    }

    - (BOOL) loadShader:(NSString*)shaderName referenceName:(NSString*)refName
    {

        NSAssert(_context, @"No Context available");

        if ([_glPrograms objectForKey:refName]) return YES;

        [EAGLContext setCurrentContext:_context];

        GLuint vertShader, fragShader;

        NSString *vertShaderPathname, *fragShaderPathname;

        // Create shader program.
        GLuint _program = glCreateProgram();

        // Create and compile vertex shader.
        vertShaderPathname = [[NSBundle mainBundle] pathForResource:shaderName ofType:@"vsh"];

        if (![self compileShader:&vertShader type:GL_VERTEX_SHADER file:vertShaderPathname]) {
            NSLog(@"Failed to compile vertex shader");
            return NO;
        }

        // Create and compile fragment shader.
        fragShaderPathname = [[NSBundle mainBundle] pathForResource:shaderName ofType:@"fsh"];

        if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER file:fragShaderPathname]) {
            NSLog(@"Failed to compile fragment shader");
            return NO;
        }

        // Attach vertex shader to program.
        glAttachShader(_program, vertShader);

        // Attach fragment shader to program.
        glAttachShader(_program, fragShader);

        // Bind attribute locations.
        // This needs to be done prior to linking.
        glBindAttribLocation(_program, GLKVertexAttribPosition, "a_position");
        glBindAttribLocation(_program, GLKVertexAttribNormal, "a_normal");
        glBindAttribLocation(_program, GLKVertexAttribTexCoord0, "a_texCoord");

        // Link program.
        if (![self linkProgram:_program]) {

            NSLog(@"Failed to link program: %d", _program);

            if (vertShader) {
                glDeleteShader(vertShader);
                vertShader = 0;
            }
            if (fragShader) {
                glDeleteShader(fragShader);
                fragShader = 0;
            }
            if (_program) {
                glDeleteProgram(_program);
                _program = 0;
            }

            return NO;

        }

        // Release vertex and fragment shaders.
        if (vertShader) {
            glDetachShader(_program, vertShader);
            glDeleteShader(vertShader);
        }

        if (fragShader) {
            glDetachShader(_program, fragShader);
            glDeleteShader(fragShader);
        }

        TDShaderSet *_newSet = [[TDShaderSet alloc] initWithProgramId:_program];

        [_glPrograms setValue:_newSet forKey:refName];

        return YES;
    }

    - (BOOL) compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file
    {

        GLint status;
        const GLchar *source;

        source = (GLchar *)[[NSString stringWithContentsOfFile:file encoding:NSUTF8StringEncoding error:nil] UTF8String];
        if (!source) {
            NSLog(@"Failed to load vertex shader");
            return NO;
        }

        *shader = glCreateShader(type);
        glShaderSource(*shader, 1, &source, NULL);
        glCompileShader(*shader);

    #if defined(DEBUG)
        GLint logLength;
        glGetShaderiv(*shader, GL_INFO_LOG_LENGTH, &logLength);
        if (logLength > 0) {
            GLchar *log = (GLchar *)malloc(logLength);
            glGetShaderInfoLog(*shader, logLength, &logLength, log);
            NSLog(@"Shader compile log:\n%s", log);
            free(log);
        }
    #endif

        glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
        if (status == 0) {
            glDeleteShader(*shader);
            return NO;
        }

        return YES;
    }

    - (BOOL) linkProgram:(GLuint)prog
    {
        GLint status;
        glLinkProgram(prog);

    #if defined(DEBUG)
        GLint logLength;
        glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
        if (logLength > 0) {
            GLchar *log = (GLchar *)malloc(logLength);
            glGetProgramInfoLog(prog, logLength, &logLength, log);
            NSLog(@"Program link log:\n%s", log);
            free(log);
        }
    #endif

        glGetProgramiv(prog, GL_LINK_STATUS, &status);
        if (status == 0) {
            return NO;
        }

        return YES;
    }

    - (BOOL) validateProgram:(GLuint)prog
    {
        GLint logLength, status;

        glValidateProgram(prog);
        glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);

        if (logLength > 0) {
            GLchar *log = (GLchar *)malloc(logLength);
            glGetProgramInfoLog(prog, logLength, &logLength, log);
            NSLog(@"Program validate log:\n%s", log);
            free(log);
        }

        glGetProgramiv(prog, GL_VALIDATE_STATUS, &status);

        if (status == 0) {
            return NO;
        }

        return YES;
    }

    #pragma mark - Singleton stuff... Don't mess with this other than proxyInit!

    - (void) proxyInit
    {

        _glPrograms = [[NSMutableDictionary alloc] init];

    }

    - (id) init
    {
        Class myClass = [self class];
        @synchronized(myClass) {
            if (!_sharedSingleton) {
                if (self = [super init]) {
                    _sharedSingleton = self;
                    [self proxyInit];
                }
            }
        }
        return _sharedSingleton;
    }

    + (TDProgamManager *) sharedInstance
    {
        @synchronized(self) {
            if (!_sharedSingleton) {
                _sharedSingleton = [[self alloc] init];
            }
        }
        return _sharedSingleton;
    }

    + (TDProgamManager *) sharedInstanceWithContext:(EAGLContext*)context
    {
        @synchronized(self) {
            if (!_sharedSingleton) {
                _sharedSingleton = [[self alloc] init];
            }
            _sharedSingleton->_context = context;
        }
        return _sharedSingleton;
    }

    + (id) allocWithZone:(NSZone *)zone
    {
        @synchronized(self) {
            if (!_sharedSingleton) {
                return [super allocWithZone:zone];
            }
        }
        return _sharedSingleton;
    }

    + (id) copyWithZone:(NSZone *)zone
    {
        return self;
    }

@end

请注意,一旦数据空间(属性/统一变量)被传入,您不必在每个渲染周期中都传入它们,而只需要在失效时传入。这将导致GPU性能的显著提升。
就VBO方面而言,上面的答案已经解释了如何最好地处理它。就方向方程而言,您需要一个机制来嵌套tdobjects(类似于iOS下的UIView和子视图),然后评估相对于父对象的旋转等。
祝你好运!

0

我很希望能够对这篇较旧的帖子做出贡献,因为我采用了一种不同的方法来解决这个问题。和提问者一样,我看到了很多“单对象”的例子。我尝试将所有顶点放入单个VBO中,然后保存该对象位置的偏移量(每个对象),而不是缓冲区句柄。它起作用了。可以将偏移量作为参数传递给glDrawElements,如下所示。回想起来似乎很明显,但在我看到它工作之前我并不确定。请注意,我一直在使用“顶点指针”,而不是更常见的“顶点属性指针”。我正在努力转向后者,以便能够利用着色器。 在调用“draw elements”之前,所有对象都会“绑定”到相同的顶点缓冲区。

        gl.glVertexPointer( 3, GLES20.GL_FLOAT, 0, vertexBufferOffset );

        GLES20.glDrawElements(
                GLES20.GL_TRIANGLES, indicesCount,
                GLES20.GL_UNSIGNED_BYTE, indexBufferOffset
        );

我没有找到任何地方明确说明这个偏移量的目的,所以我冒了个险。另外,还有一个注意点:你必须以字节为单位指定偏移量,而不是顶点或浮点数。也就是说,要乘以四才能得到正确的位置。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接