OpenGL 4 Shading Language Cookbook chapter 1——Getting Started with GLSL

sending data to a shader using vertex attributes and vertex buffer objects

the vertex shader is invoked once per vertex. its main job is to process the data associated with the vertex, and pass it (and possibly other information) along to the next stage of the pipeline. in order to give our vertex shader sth. to work with, we must have some way of providing (per-vertex) input to the shader. typically, this includes the vertex position, normal vector, and texture coordinates (among other things).

in earlier versions of opengl (prior to 3.0), each piece of vertex information had a specific “channel” in the pipeline.
it provided to the shaders using functions such as glVertex, glTexCoord, and glNormal (or within client vertex arrays using glVertexPointer, glTexCoordPointer, or glNormalPointer).

the shader would then access these values via built-in variables such as gl_Vertex and gl_Normal. this functionality was deprecated in opengl 3.0 and later removed.
instead, vertex information must now be provided using generic vertex attribtues, usually in conjunction with (vertex) buffer objects.
the programmer is now free to define an arbitrary set of per-vertex attributes to provide as input to the vertex shader.
for example, in order to implement normal mapping, the programmer might decide that position, normal vector and tangent vector should be provided along with each vertex.
with opengl 4, it’s easy to define this as the set of input attributes. this gives us a great deal of flexibility of define our vertex information in any way that is appropriate for our application, but may require a bit of getting used to for those of us who are used to the old way of doing things.

in the vertex shader, the per-vertex input attributes are defined by using the GLSL qualifier in. for example, to define a 3-component vector input attribute named VertexColor. we use the following code:

in vec3 VertexColor;

of course, the data for this attribute must be supplied by the opengl program. to do so, we make use of vertex buffer objects. the buffer object contains the values for the input attribute. in the main opengl program we make the connection between the buffer and the input attribute and define how to “step through” the data. then, when rendering, opengl pulls data for the input attribute from the buffer for each invocation of the vertex shader.

For this recipe, we’ll draw a single triangle. Our vertex attributes will be position and color.
We’ll use a fragment shader to blend the colors of each vertex across the triangle to produce an image similar to the one shown as follows. The vertices of the triangle are red, green, and blue, and the interior of the triangle has those three colors blended together. The colors may not be visible in the printed text, but the variation in the shade should indicate the blending.
在这里插入图片描述
we will start with an empty opengl program, and the following shaders:
The vertex shader (basic.vert):

#version 430
layout (location=0) in vec3 VertexPosition;
layout (location=1) in vec3 VertexColor;
out vec3 Color;
void main()
{
	Color = VertexColor;
	gl_Position = vec4(VertexPosition,1.0);
}

Attributes are the input variables to a vertex shader. In the previous code, there are two input attributes: VertexPosition and VertexColor. They are specified using the GLSL keyword in. Don’t worry about the layout prefix, we’ll discuss that later. Our main OpenGL program needs to supply the data for these two attributes for each vertex. We will do so by mapping our polygon data to these variables.

It also has one output variable named Color, which is sent to the fragment shader. In this case, Color is just an unchanged copy of VertexColor. Also, note that the attribute VertexPosition is simply expanded and passed along to the built-in output variable gl_Position for further processing.

The fragment shader (basic.frag):

#version 430
in vec3 Color;
out vec4 FragColor;
void main() 
{
	FragColor = vec4(Color, 1.0);
}

There is just one input variable for this shader, Color. This links to the corresponding output variable in the vertex shader, and will contain a value that has been interpolated across the triangle based on the values at the vertices. We simply expand and copy this color to the
output variable FragColor (more about fragment shader output variables in later recipes). Write code to compile and link these shaders into a shader program (see “Compiling a shader” and “Linking a shader program”). In the following code, I’ll assume that the handle to the shader program is programHandle.

use the following steps to set up your buffer objects and render the triangle:

use the following steps to set up your buffer objects and render the triangle:

  1. create a global (or private instance) variable to hold our handle to the vertex array object:
GLuint vaoHandle;
  1. within the initialization function, we create and populate the vertex buffer objects for each attribute:
float positionData[] = {
-0.8f, -0.8f, 0.0f,
0.8f, -0.8f, 0.0f,
0.0f, 0.8f, 0.0f };
float colorData[] = {
1.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 1.0f };
// Create and populate the buffer objects
GLuint vboHandles[2];
glGenBuffers(2, vboHandles);
GLuint positionBufferHandle = vboHandles[0];
GLuint colorBufferHandle = vboHandles[1];
// Populate the position buffer
glBindBuffer(GL_ARRAY_BUFFER, positionBufferHandle);
glBufferData(GL_ARRAY_BUFFER, 9 * sizeof(float),
positionData, GL_STATIC_DRAW);
// Populate the color buffer
glBindBuffer(GL_ARRAY_BUFFER, colorBufferHandle);
glBufferData(GL_ARRAY_BUFFER, 9 * sizeof(float), colorData,
GL_STATIC_DRAW);
  1. create and define a vertex array object, which defines the relationship between the buffers and the input attributes. (See “There’s more…” for an alternate way to do this that is valid for OpenGL 4.3 and later.)
// Create and set-up the vertex array object
glGenVertexArrays( 1, &vaoHandle );
glBindVertexArray(vaoHandle);
// Enable the vertex attribute arrays
glEnableVertexAttribArray(0); // Vertex position
glEnableVertexAttribArray(1); // Vertex color
// Map index 0 to the position buffer
glBindBuffer(GL_ARRAY_BUFFER, positionBufferHandle);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, NULL);
// Map index 1 to the color buffer
glBindBuffer(GL_ARRAY_BUFFER, colorBufferHandle);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, NULL);
  1. In the render function, we bind to the vertex array object and call glDrawArrays to initiate rendering:
glBindVertexArray(vaoHandle);
glDrawArrays(GL_TRIANGLES, 0, 3 );

vertex attributes are the input variables to our vertex shader. in the given vertex sahder, our two attribtues are VertexPosition and VertexColor. the main opengl program refers to vertex attributes by associating each (active) input variable with a generic attribute index. these generic indices are simply integers between 0 and GL_MAX_VERTEX_ATTRIBS-1. we can specify the relationship between these indices and the attributes using the layout qualifier. for example, in our vertex shader, we use the layout qualifier to assign VertexPosition to attribute index 0 and VertexColor to attribute index 1.

layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexColor;

We refer to the vertex attributes in our OpenGL code, by referring to the corresponding generic vertex attribute index.

it is not strictly necessary to explicitly specify the mappings between attribtue variables and generic attribute indexes, because opengl will automatically map active vertex attributes to generic indexes when the program is linked. we could then query for the mappings and determine the indexes that coorespond to the shader’s input variables. it may be somewhat more clear however, to explicitly specify the mapping as we do in this example.

the first step involves setting up a pair of buffer objects to store our position and color data. as with most opengl objects, we start by creating the objects and acquiring handles to th etwo buffers by calling glGenBuffers. we then assign each handle to a separate descriptive variable to make the following code more clear.

for each buffer object, we first bind the buffer to the GL_ARRAY_BUFFER binding point by calling glBindBuffer. the first argument to glBindBuffer is the target binding point. in this case, since the data is essentially a generic array, we use GL_ARRAY_BUFFER. examples of other kinds of targets (such as GL_UNIFORM_BUFFER, or GL_ELEMENT_ARRAY_BUFFER) will be seen in later examples. once our buffer object is bound, we can populate the buffer with our vertex/color data by calling glBufferData. the second and third arguments to this function are the size of the array and a pointer to the array containing the data. let us focus on the first and last arguments. the first argument indicates the target buffer object. the data provided in the third arugment is copied into the buffer that is bound to this binding point. the last argument is one that gives a opengl a hint about how the data will be used so that it can determine how best to manage the buffer intenally. for full detail about this argument, take a look into the opengl documentation. in our case, the data is specified once, will not be modified, and will be used many times for drawing operations, so this usage pattern best corresponds to the value GL_STATIC_DRAW.

now that we have set up our buffer objects 缓冲对象, we tie them together into a vertex array object (VAO). 顶点数组对象
the VAO contains information about the connections between the data in our buffers and the input vertex attributes.
we create a VAO using the function glGenVertexArrays. this gives us a handle to our new object, which we store in the (global) variable valHandle. then we enable the generic vertex attribute indexes 0 and 1 by calling glEnableVertexAttribArray. doing so indicates that that the values for the attributes will be accessed and used for rendering.

the next step makes the connection between the buffer objects and the generic vertex attribute indexes.

// Map index 0 to the position buffer
glBindBuffer(GL_ARRAY_BUFFER, positionBufferHandle);
glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, 0, NULL );

first we bind the buffer object to the GL_ARRAY_BUFFER binding point, then we call glVertexAttribPointer, which tells opengl which generic index that the data should be used with, the format of the data stored in the buffer object, and where it is located within the buffer object that is bound to the GL_ARRAY_BUFFER binding point.
the first argument is the generic attribute index.
the second is the number of components per vertex attribute (1,2,3 or 4).
in this case, we are providing 3-dimensional data, so we want 3 components per vertex.
the third argument is the data type of each component in the buffer.
the fourth is a Boolean which specifies whether or not the data should be automatically normalized (mapped to a range of [-1,1] for signed integral values or [0,1] for unsigned integral values).
the fifth argument is the stride, which indicates the byte offset between consecutive attributes.
since our data is tightly packed, we can use a value of zero.
the last arugment is a pointer, which is not treated as a pointer! instead, its value is interpreted as a byte offset from the begining of the buffer to the first attribute in the buffer. in this case, there is no additional data in either buffer before the first element, so we use a value of zero.

the glVertexAttribPointer function stores (in the VAO’s state) a pointer to the buffer currently bound to the GL_ARRAY_BUFFER binding point. when another buffer is bound to that binding point, it does not change the value of the pointer.

the VAO stores all of the opengl state related to the relationship between buffer objects and the generic vertex attributes, as well as the information about the format of the data in the buffer objects. this allows us to quickly return all of this state when rendering.
the VAO is an extremely important concept, but can be tricky to understand. it is important to remember that the VAO’s state is primarily associated with the enabled attributes and their connection to buffer obects.
it does not necessary keep track of buffer bindings.
for example, it does not remember what is bound to the GL_ARRAY_BUFFER binding point. we only bind to this point in order to set up the pointers via glVertexAttribPointer.

once we have the VAO set up (a one-time operation), we can issue a draw command to render our image.
in our render function, we clear the color buffer using glClear, bind to the vertex array object, and call glDrawArrays to draw our triangle. the function glDrawArrays initiates rendering of primitives by stepping through the buffers for each enabled attribute array, and passing the data down the pipeline to our vertex shader. the first arguement is the render mode (in this case we are drawing triangles), the second is the starting index in the enabled arrays, and the third argument is the number of indices to be rendered (3 vertexes for a single triangle).

to summarize, we followed these steps:

  1. make sure to specify the generic vertex attribute indexes for each attribute in the vertex shader using the layout qualifier.
  2. create and populate the buffer objects for each attribute.
  3. create and define the vertex array object by calling glVertexAttribPointer while the appropriate buffer is bound.
  4. when rendering, bind to the vertex array object and call glDrawArrays, or other appropriate rendering function (e.g. glDrawElements).

in the following, we will discuss some details, extensions, and alternatives to the previous technique.

Sending data to a shader using uniform variables

vertex attributes provide one avenue for providing input to shaders;
a second technique is uniform variables.

uniform variables are intended to be used for data that may change relatively infrequently compared to per-vertex attributes.
in fact, it is simply not possibe to set per-vertex attributes with uniform variables.
for example, uniform variables are well suited for the matrices used for modeling, viewing, and projective transformations.

within a shader, uniform variables are read-only.
their values can only be changed from outside the shader, via the opengl api.
however, they can be initialized within the shader by assigning to a constant value along with the declaration.

uniform variables can appear in any shader within a shader program, and are always used as input variables.
they can be declared in one or more shaders within a program. but if a variable with a given name is declared in more than one shader, its type must be the same in all shaders.
in other words, the uniform variables are held in a shared uniform namespace for the entire shader program.

in this recipe, we will draw the same triangle as in previous recipes in this chapter, however, this time, we will rotate the triangle using a uniform matrix variable.
We’ll use the following vertex shader:

#version 430
layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexColor;
out vec3 Color;
uniform mat4 RotationMatrix;
void main()
{
	Color = VertexColor;
	gl_Position = RotationMatrix * vec4(VertexPosition,1.0);
}

note the variable RotationMatrix is declared using the uniform qualifier.
we will provide the data for this variable via the opengl program.
the RotationMatrix is also used to transform VertexPosition before assigning it to the default output position variable gl_Position.

We’ll use the same fragment shader as in previous recipes:

#version 430
in vec3 Color;
layout (location = 0) out vec4 FragColor;
void main()
{
	FragColor = vec4(Color, 1.0);
}

within the main opengl code, we determine the rotation matrix and send it to the shader’s uniform variable.
to create our rotation matrix, we will use the GLM library (see the using the GLM for mathematics recipe in this chapter).
within the main opengl code, add the following include statements:

#include <glm/glm.hpp>
using glm::mat4;
using glm::vec3;
#include <glm/gtc/matrix_transform.hpp>

we will also assume that code has been written to compile and linkthe shaders, and to create the vertex array object for the color triangle.
we will assume that the handle to the vertex array is vaoHandle, and the handle to the program object is programHandle.

Within the render method, use the following code:

glClear(GL_COLOR_BUFFER_BIT);
mat4 rotationMatrix = glm::rotate(mat4(1.0f), angle,vec3(0.0f,0.0f,1.0f));
GLuint location = glGetUniformLocation(programHandle,"RotationMatrix");
if( location >= 0 )
{
	glUniformMatrix4fv(location, 1, GL_FALSE, &rotationMatrix[0][0]);
}
glBindVertexArray(vaoHandle);
glDrawArrays(GL_TRIANGLES, 0, 3 );

how it works

the steps involved with setting the value of a uniform variable including finding the location of the variable, then assigning a value to that loation using one of the glUniform functions.

in this example, we start by clearing the color buffer, then creating a rotation matrix using GLM.
next, we quary for the location of the uniform variable by calling glGetUniformLocation.
this function takes the handle to the shader program object, and the name of the uniform variable and returns its location.
if the uniform variable is not an active uniform variable, the function returns -1.

we then assign a value to the uniform variables’s location using glUniformMatrix4fv.
the first argument is the uniform variable’s location. the second is the number of matices that are being assigned (note that the uniform variable could be an array).
the third is a Boolean value indicating whether or not the matrix should be transposed when loaded into the uniform variable.

with GLM matrices, a transpose is not required, so we use GL_FALSE here. if u were implementating the matrix using an array, and the data was in row-major order, u might need to use GL_TRUE for this argument.
the last argument is a pointer to the data for the uniform variable.

there is more

of course uniform variables can be any valid GLSL type incuding complex types such as arrays of structures.
opengl provides a glUniform function with the usual suffixes, appropriate for each type.
for example, to assign to a variable of type vec3, one would use glUniform3f or glUniform3fv.

for arrays, one can use the functions ending in ‘v’ to initialize multiples within the array.
note that if it is desired, one can query for the location of a particular element of the uniform array using the [] operator.
for example, to query for the location of the second element of MyArray:

GLuint location = glGetUniformLocation( programHandle, "MyArray[1]" );

for structures, the memebers of the structure must be initialized individually. as with arrays, one can query for the location of a member of a structure using something like the following:

GLuint location = glGetUniformLocation( programHandle, "MyMatrices.Rotation" );

where the structure variable is MyMatrices and the member of the structure is Rotation.

getting a list of active uniform variables

while it is a simple process to query for the location of an individual uniform variable, there may be instances where it can be userful to generate a list of all active uniform variables.
for example, one might choose to create a set of variables to store the location of each uniform and assign their values after the program is linked.
this would avoid the need to query for uniform locations when setting the value of the uniform variables, creating slightly more efficient code.

The process for listing uniform variables is very similar to the process for listing attributes (see the Getting a list of active vertex input attributes and locations recipe), so this recipe will refer the reader back to the previous recipe for detailed explanation.

start with a basic opengl program that compiles and links a shader program. in the following, we will assume that the handle to the program is in a variable named programHandle.

after linking and enabling the shader program, use the following code to display the list of active uniforms:

  1. starting by querying for the number of active uniform variables:
GLint numUniforms = 0;
glGetProgramInterfaceiv( handle, GL_UNIFORM,GL_ACTIVE_RESOURCES, &numUniforms);
  1. loop through each unfiorm index and query for the length of the name, the type, the location and the block index:
GLenum properties[] = {GL_NAME_LENGTH, GL_TYPE, GL_LOCATION, GL_BLOCK_INDEX};
printf("Active uniforms:\n");
for( int i = 0; i < numUniforms; ++i ) 
{
	GLint results[4];
	glGetProgramResourceiv(handle, GL_UNIFORM, i, 4, properties, 4, NULL, results);
	if( results[3] != -1 )
		continue; // Skip uniforms in blocks
	GLint nameBufSize = results[0] + 1;
	char * name = new char[nameBufSize];
	glGetProgramResourceName(handle, GL_UNIFORM, i, nameBufSize, NULL, name);
	printf("%-5d %s (%s)\n", results[2], name, getTypeString(results[1]));
	delete [] name;
}

the process is very similar to the process shown in the recipe getting a list of active vertex input attributes and locations. i will focus on the main differences.

first and most obvious is that we use GL_UNIFORM instead of GL_PROGRAM_INPUT as the interface that we are querying in glGetProgramResourceiv and glGetProgramInterfaceiv.
second, we query for the block index (using GL_BLOCK_INDEX in the properties array). the reason for this is that some uniform variables are contained within a uniform block (see the recipe using uniform blocks and uniform buffer objects).
for this example, we only want information about uniforms that are not within blocks. the block index will be -1 if the uniform variable is not within a block, so we skip any uniform variables that do not have a block index of -1.

again, we use the getTypeString function to convert the type value into a human-readable string (see example code).
when this is run on the shader program from the previous recipe, we see the following output:

Active uniforms:
0 RotationMatrix (mat4)

as with vertex attributes, a uniform variable is not considered active unless it is determined by the glsl linker that it will be used within the shader.

the previous code is only avalid for opengl 4.3 and later. alternatively, u can achieve similar results using the functions glGetProgramiv, glGetActiveUniform, glGetUniformLocation, and glGetActiveUniformName.

using uniform blocks and uniform buffer objects
if your program involves multiple shader programs that use the same uniform variables, one has to manage the variables separately for each program. uniform locations are generated when a program is linked, so the lcoations of the uniforms may change from one program to the next. the data for those uniforms may have to be regenerated and applied to the new locations.

uniform block were desinged to ease the sharing of uniform data between programs. with uniform blocks, one can create a buffer object for storing the values of all the uniform variables, and bind the buffer to the uniform block. when changing programs, the same buffer object need only be re-bound to the corresponding block in the new program.
这个可参考:https://blog.csdn.net/wangdingqiaoit/article/details/52717963

a uniform block is simply a group of uniform variables defined within a syntactical 语法的 structure known as a uniform block. for example, in this recipe, we will use the following uniform block:

uniform BlobSettings 
{
	vec4 InnerColor;
	vec4 OuterColor;
	float RadiusInner;
	float RadiusOuter;
};

this defines a block with the name BlobSettings that contains four uniform variables. with this type of block definition, the variables within the block are still part of the global scope and do not need to be qualified with the block name.

the buffer object used to store the data for the uniforms is often referred to as a uniform buffer object. we will see that a uniform buffer object is simply just a better object that is bound to a certain location.

for this recipe, we will use a simple example to demonstrate the use of uniform buffer objects and uniform blocks. we will draw a quad (two triangles) with texture coordinates, and use our fragment shader to fill the quad with a fuzzy circle. the circle is a solide color in the center, but at its edge, it gradually fades to the background color, as shown in the following image:
在这里插入图片描述

start with an opengl program that draws two triangles to form a quad. provide the position at vertex attribute location 0, and the texture coordiante (0 to 1 in each direction) at vertex attribute location 1 (see the sending data to a shader using vertex attributes and vertex buffer objects recipe).

we will use the following vertex shader:

#version 430
layout (location = 0) in vec3 VertexPosition;
layout (location = 1) in vec3 VertexTexCoord;
out vec3 TexCoord;
void main()
{
	TexCoord = VertexTexCoord;
	gl_Position = vec4(VertexPosition,1.0);
}

the fragment shader contains the uniform block, and is responsible for drawing our fuzzy circle:

#version 430
in vec3 TexCoord;
layout (location = 0) out vec4 FragColor;
layout (binding = 0) uniform BlobSettings 
{
	vec4 InnerColor;
	vec4 OuterColor;
	float RadiusInner;
	float RadiusOuter;
};
void main() {
	float dx = TexCoord.x - 0.5;
	float dy = TexCoord.y - 0.5;
	float dist = sqrt(dx * dx + dy * dy);
	FragColor =
	mix( InnerColor, OuterColor, smoothstep( RadiusInner, RadiusOuter, dist ));
}

note the uniform block named BlobSettings. the variables within this block define the parameters of our fuzzy circle. the variable OuterColor defines the color outside of the circle. InnerColor is the color inside of the circle. RadiusInner is the radius defining the part of the circle that is a solid color (inside the fuzzy edge), and the distance from the center of the circle to the inner edge of the fuzzy boundary. RadiusOuter is the outer edge of the fuzzy boundary of the circle (when the color is equal to OuterColor).

the code within the main function computes the distance of the texture coordiante to the center of the quad located at (0.5, 0.5).
it then uses that distance to compute the color by using the smoothstep function. this function provides a value at smoothy varies between 0.0 and 1.0 when the value of the third argument is betwen the values of the first two arguments. Otherwise it returns 0.0 or 1.0 depending on whether dist is less than the first or greater than the second, respectively. The mix function is then used to linearly interpolate between
InnerColor and OuterColor based on the value returned by the smoothstep function.

In the OpenGL program, after linking the shader program, use the following steps to assign
data to the uniform block in the fragment shader:

  1. get the index of the uniform block using glGetUniformBlockIndex.
GLuint blockIndex = glGetUniformBlockIndex(programHandle, "BlobSettings");
  1. allocate space for the buffer to contain the data for the uniform block. we get the size using glGetActiveUniformBlockiv:
GLint blockSize;
glGetActiveUniformBlockiv(programHandle, blockIndex, GL_UNIFORM_BLOCK_DATA_SIZE, &blockSize);
GLubyte * blockBuffer;
blockBuffer = (GLubyte *) malloc(blockSize);
  1. query for the offset of each variable within the block. to do so, we first find the index of each variable within the block:
const GLchar *names[] = { "InnerColor", "OuterColor", "RadiusInner", "RadiusOuter" };
GLuint indices[4];
glGetUniformIndices(programHandle, 4, names, indices);
GLint offset[4];
glGetActiveUniformsiv(programHandle, 4, indices, GL_UNIFORM_OFFSET, offset);
  1. place the data into the buffer at the appropriate offsets:
// Store data within the buffer at the appropriate offsets
GLfloat outerColor[] = {0.0f, 0.0f, 0.0f, 0.0f};
GLfloat innerColor[] = {1.0f, 1.0f, 0.75f, 1.0f};
GLfloat innerRadius = 0.25f, outerRadius = 0.45f;
memcpy(blockBuffer + offset[0], innerColor, 4 * sizeof(GLfloat));
memcpy(blockBuffer + offset[1], outerColor, 4 * sizeof(GLfloat));
memcpy(blockBuffer + offset[2], &innerRadius, sizeof(GLfloat));
memcpy(blockBuffer + offset[3], &outerRadius, sizeof(GLfloat));
  1. create the buffer object and copy the data into it:
GLuint uboHandle;
glGenBuffers( 1, &uboHandle );
glBindBuffer( GL_UNIFORM_BUFFER, uboHandle );
glBufferData( GL_UNIFORM_BUFFER, blockSize, blockBuffer, GL_DYNAMIC_DRAW );
  1. bind the buffer object to the uniform buffer binding point at the index specified by the binding layout qualifier in the fragment shader (0):
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboHandle);

phew! this seems like a lot of work! however, the real advantage comes when using multiple programs where the same buffer object can be used for each program. let us take a look at each step individually.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值