关闭

【LWJGL2 WIKI】【现代OpenGL篇】用BufferSubData更新VBO方形

标签: javalwjgl
529人阅读 评论(0) 收藏 举报
分类:

原文:http://wiki.lwjgl.org/wiki/The_Quad_updating_a_VBO_with_BufferSubData

Introduction 介绍

VBO一旦设置好,我们可以随意去画它许多次,不用在意实际数据会变成啥样,因为一旦数据已经传至显存,OpenGL了解数据的位置并且有必要的话自动会去取。不过我们也可能需要去调整模型顶点以实现动画之类的效果。

Creating our VBO 创建VBO

前边已经讲过怎样设置VBO了,调用glBufferData在显卡上分配内存,然后将字节上传上去,还要告诉OpenGL怎样去使用这些数据,也就是声明GL_STATIC_DRAW。如果后续还要再更新VBO,我们应该声明的是GL_STREAM_DRAW,这样OpenGL就知道数据接下来应该如何相应处理了。
像下面这样创建VBO,verticesFloatBuffer里面存的是顶点数据。

// Create a new Vertex Buffer Object in memory and select it (bind)
vboId = GL15.glGenBuffers();
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
GL15.glBufferData(GL15.GL_ARRAY_BUFFER, verticesFloatBuffer, GL15.GL_STREAM_DRAW);

Updating our VBO 更新VBO

更新VBO时,应该先保证OpenGL目前没在使用VBO渲染,因此要在渲染代码前更新VBO。为了更新VBO只需要再次绑定,创建新的顶点定义字节然后用glBufferSubData上传它们。glBufferData分配显存的地方将被glBufferSubData覆盖重用。我们将给一个在X和Y方向的随机的偏移量来调整我们的方形顶点。

// Update vertices in the VBO, first bind the VBO
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);

// Apply and update vertex data
for (int i = 0; i < vertices.length; i++) {
    TexturedVertex vertex = vertices[i];

    // Define offset
    float offsetX = (float) (Math.cos(Math.PI * Math.random()) * 0.1);
    float offsetY = (float) (Math.sin(Math.PI * Math.random()) * 0.1);

    // Offset the vertex position
    float[] xyz = vertex.getXYZ();
    vertex.setXYZ(xyz[0] + offsetX, xyz[1] + offsetY, xyz[2]);

    // Put the new data in a ByteBuffer (in the view of a FloatBuffer)
    FloatBuffer vertexFloatBuffer = vertexByteBuffer.asFloatBuffer();
    vertexFloatBuffer.rewind();
    vertexFloatBuffer.put(vertex.getElements());
    vertexFloatBuffer.flip();

    GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, i * TexturedVertex.stride,
            vertexByteBuffer);

    // Restore the vertex data
    vertex.setXYZ(xyz[0], xyz[1], xyz[2]);
}

// And of course unbind
GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);

注意我们一次只更新了一个顶点,glBufferSubData并不意味着要一次重写全部的字节,并非在方形的内存块里,而是用步进(一个顶点类里定义的字节数)来决定有多少个字节要重写偏移。将被在我们的方形缓冲区里重写的字节数,跟每个单独顶点缓冲区的字节数相同。
(译注:这段英文直译实在是译不懂,总之大概意思就是说glBufferSubData的重写其实是很灵活的,它这里利用了在顶点类里定义的步进值stride来实现了每单个顶点的重写更新,之所以这样是因为它希望实现的效果是四个顶点的随机偏移量各不相同,这样最后就是疯狂变形乱颤的效果。如果要是只取一次随机数,然后一次重写四个顶点,那么得到的就是一个始终保持平行四边形状态的方形在乱颤,可以自己试试看。并不一定非得按照这个范例中讲的这个过程来写,理解一下他想表达的意思就可以,最后还是得根据自己的需求而定。)
代码唯一需要注意的部分就是下面这行:

GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, i * TexturedVertex.stride,vertexByteBuffer);

记住,TexturedVertex类保存某个顶点的所有信息,包括构成它的字节数(即步进)

The result 结果

结果就是一个方形以随机的方式一直在动(运行程序会看到效果)
这里写图片描述

Complete source code 完整源代码

Vertex shader

#version 150 core

in vec4 in_Position;
in vec4 in_Color;
in vec2 in_TextureCoord;

out vec4 pass_Color;
out vec2 pass_TextureCoord;

void main(void) {
    gl_Position = in_Position;

    pass_Color = in_Color;
    pass_TextureCoord = in_TextureCoord;
}

Fragment shader

#version 150 core

uniform sampler2D texture_diffuse;

in vec4 pass_Color;
in vec2 pass_TextureCoord;

out vec4 out_Color;

void main(void) {
    out_Color = pass_Color;
    // Override out_Color with our texture pixel
    out_Color = texture2D(texture_diffuse, pass_TextureCoord);
}

TexturedVertex class

public class TexturedVertex {
    // Vertex data
    private float[] xyzw = new float[] {0f, 0f, 0f, 1f};
    private float[] rgba = new float[] {1f, 1f, 1f, 1f};
    private float[] st = new float[] {0f, 0f};

    // The amount of bytes an element has
    public static final int elementBytes = 4;

    // Elements per parameter
    public static final int positionElementCount = 4;
    public static final int colorElementCount = 4;
    public static final int textureElementCount = 2;

    // Bytes per parameter
    public static final int positionBytesCount = positionElementCount * elementBytes;
    public static final int colorByteCount = colorElementCount * elementBytes;
    public static final int textureByteCount = textureElementCount * elementBytes;

    // Byte offsets per parameter
    public static final int positionByteOffset = 0;
    public static final int colorByteOffset = positionByteOffset + positionBytesCount;
    public static final int textureByteOffset = colorByteOffset + colorByteCount;

    // The amount of elements that a vertex has
    public static final int elementCount = positionElementCount +
            colorElementCount + textureElementCount;   
    // The size of a vertex in bytes, like in C/C++: sizeof(Vertex)
    public static final int stride = positionBytesCount + colorByteCount +
            textureByteCount;

    // Setters
    public void setXYZ(float x, float y, float z) {
        this.setXYZW(x, y, z, 1f);
    }

    public void setRGB(float r, float g, float b) {
        this.setRGBA(r, g, b, 1f);
    }

    public void setST(float s, float t) {
        this.st = new float[] {s, t};
    }

    public void setXYZW(float x, float y, float z, float w) {
        this.xyzw = new float[] {x, y, z, w};
    }

    public void setRGBA(float r, float g, float b, float a) {
        this.rgba = new float[] {r, g, b, 1f};
    }

    // Getters 
    public float[] getElements() {
        float[] out = new float[TexturedVertex.elementCount];
        int i = 0;

        // Insert XYZW elements
        out[i++] = this.xyzw[0];
        out[i++] = this.xyzw[1];
        out[i++] = this.xyzw[2];
        out[i++] = this.xyzw[3];
        // Insert RGBA elements
        out[i++] = this.rgba[0];
        out[i++] = this.rgba[1];
        out[i++] = this.rgba[2];
        out[i++] = this.rgba[3];
        // Insert ST elements
        out[i++] = this.st[0];
        out[i++] = this.st[1];

        return out;
    }

    public float[] getXYZW() {
        return new float[] {this.xyzw[0], this.xyzw[1], this.xyzw[2], this.xyzw[3]};
    }

    public float[] getXYZ() {
        return new float[] {this.xyzw[0], this.xyzw[1], this.xyzw[2]};
    }

    public float[] getRGBA() {
        return new float[] {this.rgba[0], this.rgba[1], this.rgba[2], this.rgba[3]};
    }

    public float[] getRGB() {
        return new float[] {this.rgba[0], this.rgba[1], this.rgba[2]};
    }

    public float[] getST() {
        return new float[] {this.st[0], this.st[1]};
    }
}

Application

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL20;
import org.lwjgl.opengl.GL30;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.util.glu.GLU;

import de.matthiasmann.twl.utils.PNGDecoder;
import de.matthiasmann.twl.utils.PNGDecoder.Format;

public class TheQuadExampleUpdateVBO {
    // Entry point for the application
    public static void main(String[] args) {
        new TheQuadExampleUpdateVBO();
    }

    // Setup variables
    private final String WINDOW_TITLE = "The Quad: Update VBO";
    private final int WIDTH = 320;
    private final int HEIGHT = 320;
    // Quad variables
    private int vaoId = 0;
    private int vboId = 0;
    private int vboiId = 0;
    private int indicesCount = 0;
    // Shader variables
    private int vsId = 0;
    private int fsId = 0;
    private int pId = 0;
    // Texture variables
    private int[] texIds = new int[] {0, 0};
    private int textureSelector = 0;
    // Update VBO variables
    private TexturedVertex[] vertices = null;
    private ByteBuffer vertexByteBuffer = null;
    private ByteBuffer verticesByteBuffer = null;

    public TheQuadExampleUpdateVBO() {
        // Initialize OpenGL (Display)
        this.setupOpenGL();

        this.setupQuad();
        this.setupShaders();
        this.setupTextures();

        while (!Display.isCloseRequested()) {
            // Do a single loop (logic/render)
            this.loopCycle();

            // Force a maximum FPS of about 60
            Display.sync(60);
            // Let the CPU synchronize with the GPU if GPU is tagging behind
            Display.update();
        }

        // Destroy OpenGL (Display)
        this.destroyOpenGL();
    }

    private void setupTextures() {
        texIds[0] = this.loadPNGTexture("assets/stGrid1.png", GL13.GL_TEXTURE0);
        texIds[1] = this.loadPNGTexture("assets/stGrid2.png", GL13.GL_TEXTURE0);

        this.exitOnGLError("setupTexture");
    }

    private void setupOpenGL() {
        // Setup an OpenGL context with API version 3.2
        try {
            PixelFormat pixelFormat = new PixelFormat();
            ContextAttribs contextAtrributes = new ContextAttribs(3, 2)
                .withForwardCompatible(true)
                .withProfileCore(true);

            Display.setDisplayMode(new DisplayMode(WIDTH, HEIGHT));
            Display.setTitle(WINDOW_TITLE);
            Display.create(pixelFormat, contextAtrributes);

            GL11.glViewport(0, 0, WIDTH, HEIGHT);
        } catch (LWJGLException e) {
            e.printStackTrace();
            System.exit(-1);
        }

        // Setup an XNA like background color
        GL11.glClearColor(0.4f, 0.6f, 0.9f, 0f);

        // Map the internal OpenGL coordinate system to the entire screen
        GL11.glViewport(0, 0, WIDTH, HEIGHT);

        this.exitOnGLError("setupOpenGL");
    }

    private void setupQuad() {
        // We'll define our quad using 4 vertices of the custom 'TexturedVertex' class
        TexturedVertex v0 = new TexturedVertex();
        v0.setXYZ(-0.5f, 0.5f, 0); v0.setRGB(1, 0, 0); v0.setST(0, 0);
        TexturedVertex v1 = new TexturedVertex();
        v1.setXYZ(-0.5f, -0.5f, 0); v1.setRGB(0, 1, 0); v1.setST(0, 1);
        TexturedVertex v2 = new TexturedVertex();
        v2.setXYZ(0.5f, -0.5f, 0); v2.setRGB(0, 0, 1); v2.setST(1, 1);
        TexturedVertex v3 = new TexturedVertex();
        v3.setXYZ(0.5f, 0.5f, 0); v3.setRGB(1, 1, 1); v3.setST(1, 0);

        vertices = new TexturedVertex[] {v0, v1, v2, v3};

        // Create a FloatBufer of the appropriate size for one vertex
        vertexByteBuffer = BufferUtils.createByteBuffer(TexturedVertex.stride);

        // Put each 'Vertex' in one FloatBuffer
        verticesByteBuffer = BufferUtils.createByteBuffer(vertices.length *
                TexturedVertex.stride);            
        FloatBuffer verticesFloatBuffer = verticesByteBuffer.asFloatBuffer();
        for (int i = 0; i < vertices.length; i++) {
            // Add position, color and texture floats to the buffer
            verticesFloatBuffer.put(vertices[i].getElements());
        }
        verticesFloatBuffer.flip();


        // OpenGL expects to draw vertices in counter clockwise order by default
        byte[] indices = {
                0, 1, 2,
                2, 3, 0
        };
        indicesCount = indices.length;
        ByteBuffer indicesBuffer = BufferUtils.createByteBuffer(indicesCount);
        indicesBuffer.put(indices);
        indicesBuffer.flip();

        // Create a new Vertex Array Object in memory and select it (bind)
        vaoId = GL30.glGenVertexArrays();
        GL30.glBindVertexArray(vaoId);

        // Create a new Vertex Buffer Object in memory and select it (bind)
        vboId = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);
        GL15.glBufferData(GL15.GL_ARRAY_BUFFER, verticesFloatBuffer, GL15.GL_STREAM_DRAW);

        // Put the position coordinates in attribute list 0
        GL20.glVertexAttribPointer(0, TexturedVertex.positionElementCount, GL11.GL_FLOAT,
                false, TexturedVertex.stride, TexturedVertex.positionByteOffset);
        // Put the color components in attribute list 1
        GL20.glVertexAttribPointer(1, TexturedVertex.colorElementCount, GL11.GL_FLOAT,
                false, TexturedVertex.stride, TexturedVertex.colorByteOffset);
        // Put the texture coordinates in attribute list 2
        GL20.glVertexAttribPointer(2, TexturedVertex.textureElementCount, GL11.GL_FLOAT,
                false, TexturedVertex.stride, TexturedVertex.textureByteOffset);

        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);

        // Deselect (bind to 0) the VAO
        GL30.glBindVertexArray(0);

        // Create a new VBO for the indices and select it (bind) - INDICES
        vboiId = GL15.glGenBuffers();
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);
        GL15.glBufferData(GL15.GL_ELEMENT_ARRAY_BUFFER, indicesBuffer, GL15.GL_STATIC_DRAW);
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);

        this.exitOnGLError("setupQuad");
    }

    private void setupShaders() {      
        // Load the vertex shader
        vsId = this.loadShader("src/thequad/vertex_textured.glsl", GL20.GL_VERTEX_SHADER);
        // Load the fragment shader
        fsId = this.loadShader("src/thequad/fragment_textured.glsl", GL20.GL_FRAGMENT_SHADER);

        // Create a new shader program that links both shaders
        pId = GL20.glCreateProgram();
        GL20.glAttachShader(pId, vsId);
        GL20.glAttachShader(pId, fsId);

        // Position information will be attribute 0
        GL20.glBindAttribLocation(pId, 0, "in_Position");
        // Color information will be attribute 1
        GL20.glBindAttribLocation(pId, 1, "in_Color");
        // Textute information will be attribute 2
        GL20.glBindAttribLocation(pId, 2, "in_TextureCoord");

        GL20.glLinkProgram(pId);
        GL20.glValidateProgram(pId);

        this.exitOnGLError("setupShaders");
    }

    private void logicCycle() {
        // Texture selection
        while(Keyboard.next()) {
            // Only listen to events where the key was pressed (down event)
            if (!Keyboard.getEventKeyState()) continue;

            // Switch textures depending on the key released
            switch (Keyboard.getEventKey()) {
            case Keyboard.KEY_1:
                textureSelector = 0;
                break;
            case Keyboard.KEY_2:
                textureSelector = 1;
                break;
            }
        }

        // Update vertices in the VBO, first bind the VBO
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vboId);

        // Apply and update vertex data
        for (int i = 0; i < vertices.length; i++) {
            TexturedVertex vertex = vertices[i];

            // Define offset
            float offsetX = (float) (Math.cos(Math.PI * Math.random()) * 0.1);
            float offsetY = (float) (Math.sin(Math.PI * Math.random()) * 0.1);

            // Offset the vertex position
            float[] xyz = vertex.getXYZ();
            vertex.setXYZ(xyz[0] + offsetX, xyz[1] + offsetY, xyz[2]);

            // Put the new data in a ByteBuffer (in the view of a FloatBuffer)
            FloatBuffer vertexFloatBuffer = vertexByteBuffer.asFloatBuffer();
            vertexFloatBuffer.rewind();
            vertexFloatBuffer.put(vertex.getElements());
            vertexFloatBuffer.flip();

            GL15.glBufferSubData(GL15.GL_ARRAY_BUFFER, i * TexturedVertex.stride,
                    vertexByteBuffer);

            // Restore the vertex data
            vertex.setXYZ(xyz[0], xyz[1], xyz[2]);
        }

        // And of course unbind
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);

        this.exitOnGLError("logicCycle");
    }

    private void renderCycle() {
        GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);

        GL20.glUseProgram(pId);

        // Bind the texture
        GL13.glActiveTexture(GL13.GL_TEXTURE0);
        GL11.glBindTexture(GL11.GL_TEXTURE_2D, texIds[textureSelector]);

        // Bind to the VAO that has all the information about the vertices
        GL30.glBindVertexArray(vaoId);
        GL20.glEnableVertexAttribArray(0);
        GL20.glEnableVertexAttribArray(1);
        GL20.glEnableVertexAttribArray(2);

        // Bind to the index VBO that has all the information about the order of the vertices
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, vboiId);

        // Draw the vertices
        GL11.glDrawElements(GL11.GL_TRIANGLES, indicesCount, GL11.GL_UNSIGNED_BYTE, 0);

        // Put everything back to default (deselect)
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
        GL20.glDisableVertexAttribArray(0);
        GL20.glDisableVertexAttribArray(1);
        GL20.glDisableVertexAttribArray(2);
        GL30.glBindVertexArray(0);

        GL20.glUseProgram(0);

        this.exitOnGLError("renderCycle");
    }

    private void loopCycle() {
        // Update logic
        this.logicCycle();
        // Update rendered frame
        this.renderCycle();

        this.exitOnGLError("loopCycle");
    }

    private void destroyOpenGL() { 
        // Delete the texture
        GL11.glDeleteTextures(texIds[0]);
        GL11.glDeleteTextures(texIds[1]);

        // Delete the shaders
        GL20.glUseProgram(0);
        GL20.glDetachShader(pId, vsId);
        GL20.glDetachShader(pId, fsId);

        GL20.glDeleteShader(vsId);
        GL20.glDeleteShader(fsId);
        GL20.glDeleteProgram(pId);

        // Select the VAO
        GL30.glBindVertexArray(vaoId);

        // Disable the VBO index from the VAO attributes list
        GL20.glDisableVertexAttribArray(0);
        GL20.glDisableVertexAttribArray(1);

        // Delete the vertex VBO
        GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
        GL15.glDeleteBuffers(vboId);

        // Delete the index VBO
        GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
        GL15.glDeleteBuffers(vboiId);

        // Delete the VAO
        GL30.glBindVertexArray(0);
        GL30.glDeleteVertexArrays(vaoId);

        this.exitOnGLError("destroyOpenGL");

        Display.destroy();
    }

    private int loadShader(String filename, int type) {
        StringBuilder shaderSource = new StringBuilder();
        int shaderID = 0;

        try {
            BufferedReader reader = new BufferedReader(new FileReader(filename));
            String line;
            while ((line = reader.readLine()) != null) {
                shaderSource.append(line).append("\n");
            }
            reader.close();
        } catch (IOException e) {
            System.err.println("Could not read file.");
            e.printStackTrace();
            System.exit(-1);
        }

        shaderID = GL20.glCreateShader(type);
        GL20.glShaderSource(shaderID, shaderSource);
        GL20.glCompileShader(shaderID);

        if (GL20.glGetShader(shaderID, GL20.GL_COMPILE_STATUS) == GL11.GL_FALSE) {
            System.err.println("Could not compile shader.");
            System.exit(-1);
        }

        this.exitOnGLError("loadShader");

        return shaderID;
    }

    private int loadPNGTexture(String filename, int textureUnit) {
        ByteBuffer buf = null;
        int tWidth = 0;
        int tHeight = 0;

        try {
            // Open the PNG file as an InputStream
            InputStream in = new FileInputStream(filename);
            // Link the PNG decoder to this stream
            PNGDecoder decoder = new PNGDecoder(in);

            // Get the width and height of the texture
            tWidth = decoder.getWidth();
            tHeight = decoder.getHeight();


            // Decode the PNG file in a ByteBuffer
            buf = ByteBuffer.allocateDirect(
                    4 * decoder.getWidth() * decoder.getHeight());
            decoder.decode(buf, decoder.getWidth() * 4, Format.RGBA);
            buf.flip();

            in.close();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(-1);
        }

        // Create a new texture object in memory and bind it
        int texId = GL11.glGenTextures();
        GL13.glActiveTexture(textureUnit);
        GL11.glBindTexture(GL11.GL_TEXTURE_2D, texId);

        // All RGB bytes are aligned to each other and each component is 1 byte
        GL11.glPixelStorei(GL11.GL_UNPACK_ALIGNMENT, 1);

        // Upload the texture data and generate mip maps (for scaling)
        GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGB, tWidth, tHeight, 0,
                GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buf);
        GL30.glGenerateMipmap(GL11.GL_TEXTURE_2D);

        // Setup the ST coordinate system
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);

        // Setup what to do when the texture has to be scaled
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER,
                GL11.GL_LINEAR);
        GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER,
                GL11.GL_LINEAR_MIPMAP_LINEAR);

        this.exitOnGLError("loadPNGTexture");

        return texId;
    }

    private void exitOnGLError(String errorMessage) {
        int errorValue = GL11.glGetError();

        if (errorValue != GL11.GL_NO_ERROR) {
            String errorString = GLU.gluErrorString(errorValue);
            System.err.println("ERROR - " + errorMessage + ": " + errorString);

            if (Display.isCreated()) Display.destroy();
            System.exit(-1);
        }
    }
}

Credit

Mathias Verboven (moci)

0
0

猜你在找
深度学习基础与TensorFlow实践
【在线峰会】前端开发重点难点技术剖析与创新实践
【在线峰会】一天掌握物联网全栈开发之道
【在线峰会】如何高质高效的进行Android技术开发
机器学习40天精英计划
Python数据挖掘与分析速成班
微信小程序开发实战
JFinal极速开发企业实战
备战2017软考 系统集成项目管理工程师 学习套餐
Python大型网络爬虫项目开发实战(全套)
查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:26244次
    • 积分:660
    • 等级:
    • 排名:千里之外
    • 原创:13篇
    • 转载:0篇
    • 译文:28篇
    • 评论:1条
    文章分类
    最新评论