头图

0. Preface

Primitive API is the bottom layer of the public API, which is oriented towards high-performance, customizable material shaders ( Appearance API + FabricMaterial Specification ), and static 3D objects.

Despite this, Primitive API still encapsulates a large number of geometry classes, material classes, WebWorker, and currently only the new architecture of the 3D model class is open to the custom shader API, which has not been distributed to Primitive API .

If the API burden is not so heavy, and you want to use your own model format (it must be triangular), then the private DrawCommand + VertexArray interface is very suitable, and its style is already the closest to the bottom layer of CesiumJS WebGL. class API too.

DrawCommand is an excellent design of Cesium to encapsulate WebGL, it uses drawing data ( VertexArray ) and drawing behavior ( ShaderProgram ) as an object That is, when Scene executes the executeCommand function, all the instruction objects on the frame state object will be executed using the WebGL function, and you can bind whatever you want, so that the usage in drawing is consistent. The upper application interface only needs to generate the instruction object.

0.1. DrawCommand in the source code

For instance, in Primitive.js module createCommands function, which is responsible Primitive parametric data or calculations WebWorker object data generation combined DrawCommand where:

 function createCommands(/* 参数省略 */) {
  // ...
  const length = colorCommands.length;
  let vaIndex = 0;
  for (let i = 0; i < length; ++i) {
    let colorCommand;

    // ...

    colorCommand = colorCommands[i];
    if (!defined(colorCommand)) {
      colorCommand = colorCommands[i] = new DrawCommand({
        owner: primitive, // 入参,即 Primitive 对象
        primitiveType: primitive._primitiveType,
      });
    }
    colorCommand.vertexArray = primitive._va[vaIndex]; // VertexArray
    colorCommand.renderState = primitive._frontFaceRS; // 渲染状态
    colorCommand.shaderProgram = primitive._sp; // ShaderProgram
    colorCommand.uniformMap = uniforms; // 统一值
    colorCommand.pass = pass; // 该指令的通道顺序
  }
  // ...
}

1. Create

1.1. Components - VertexArray

Cesium wraps WebGL's vertex buffer and index buffer into Buffer , and then binds these vertex-related buffers in an object for convenience, called VertexArray , which will be enabled internally WebGL's VAO function.

The fastest way to create VertexArray is to call its static method VertexArray.fromGeometry() , but this requires Geometry API to help.

If you want to use Buffer directly to illustrate, then you have to create Buffer first:

 const positionBuffer = Buffer.createVertexBuffer({
  context: context,
  sizeInBytes: 12,
  usage: BufferUsage.STATIC_DRAW,
  typedArray: new Float32Array([/* ... */])
})
const attributes = [
  {
    index: 0,
    enabled: true,
    vertexBuffer: positionBuffer,
    componentsPerAttribute: 3,
    componentDatatype: ComponentDatatype.FLOAT,
    normalize: false,
    offsetInBytes: 0,
    strideInBytes: 0, // 紧密组合在一起,没有 byteStride
    instanceDivisor: 0 // 不实例化绘制
  }
]

Call Buffer static method of the private class createVertexBuffer() , you can create a built- WebGLBuffer vertex buffer objects positionBuffer , then use ordinary objects The array creates vertex attributes attributes , and each object describes a vertex attribute. Next, you can create these simple materials VertexArray :

 const va = new VertexArray({
  context: context,
  attributes: attributes
})

Context encapsulates various function calls of WebGL, you can get it from Scene or directly from FrameState .

Created in this step Buffer , the vertex coordinates are in the Cartesian coordinate system, which is the most primitive coordinate value, unless matrix transformation is done in the shader, or these Cartesian coordinates are near the surface of the world coordinate system. It is a bunch of coordinates with no specific semantics, purely mathematical geometry, independent of the rendering pipeline. Therefore, for a coordinate point somewhere on the surface, it is usually used in conjunction with the ENU transformation matrix + the built-in MVP transformation matrix, see the example in 1.6.

Here's another example, using two VertexAttributes:

 const positionBuffer = Buffer.createVertexBuffer({
  context: context,
  sizeInBytes: 12,
  usage: BufferUsage.STATIC_DRAW
})
const normalBuffer = Buffer.createVertexBuffer({
  context: context,
  sizeInBytes: 12,
  usage: BufferUsage.STATIC_DRAW
})
const attributes = [
  {
    index: 0,
    vertexBuffer: positionBuffer,
    componentsPerAttribute: 3,
    componentDatatype: ComponentDatatype.FLOAT
  },
  {
    index: 1,
    vertexBuffer: normalBuffer,
    componentsPerAttribute: 3,
    componentDatatype: ComponentDatatype.FLOAT
  }
]
const va = new VertexArray({
  context: context,
  attributes: attributes
})

Here, the coordinate buffer and the normal buffer are stored separately in two objects. In fact, WebGL can use the byte-interleaved format to combine all the vertex attribute buffers into one, so I won't go into details. Readers can do it by themselves. Check out the usage of WebGLBuffer in WebGL.

1.2. Components - ShaderProgram

The shader of WebGL is also encapsulated by CesiumJS, which has its own caching mechanism, and uses a large number of regular methods to match, parse and manage shader source code.

The shader code is managed by ShaderSource , and ShaderProgram manages multiple shader source codes, that is, the shader itself. Use ShaderCache as a cache container for shader programs. Their hierarchical relationship is as follows:

 Context
  ┖ ShaderCache
    ┖ ShaderProgram
      ┖ ShaderSource

You can create your own ShaderSource , ShaderProgram and add ---3f91880994ddaa72a06aee5bb4ddbfe1 Context to ShaderCache .

Example:

 new ShaderSource({
  sources : [GlobeFS]
})

new ShaderProgram({
  gl: context._gl,
  logShaderCompilation: context.logShaderCompilation,
  debugShaders: context.debugShaders,
  vertexShaderSource: vertexShaderSource,
  vertexShaderText: vertexShaderText,
  fragmentShaderSource: fragmentShaderSource,
  fragmentShaderText: fragmentShaderText,
  attributeLocations: attributeLocations,
})

But usually the more direct way is chosen:

 const vertexShaderText = `attribute vec3 position;
void main() {
  gl_Position = czm_projection * czm_modelView * vec4(position, 1.0);
}`
const fragmentShaderText = `uniform vec3 color;
void main() {
  gl_FragColor=vec4( color , 1. );
}`

const program = ShaderProgram.fromCache({
  context: context,
  vertexShaderSource: vertexShaderText,
  fragmentShaderSource: fragmentShaderText,
  attributeLocations: attributeLocations
})

Using the ShaderProgram.fromCache static method will automatically cache the shader for you in the ShaderCache container.

Shader code can directly use built-in constants and auto-uniform values, which are added by default.

attributeLocation what is it? It's a very plain JavaScript object:

 {
  "position": 0,
  "normal": 1,
  "st": 2,
  "bitangent": 3,
  "tangent": 4,
  "color": 5
}

It indicates the location of the vertex attribute in the shader.

1.3. Constituents - WebGL Uniform Values

This one is relatively simple:

 const uniforms = {
  color() {
    return Cesium.Color.HONEYDEW 
  }
}

Just use a JavaScript object, each member must be a method , and the returned value meets the requirements of Uniform:

  • Cesium.Matrix2/3/4mat2/3/4
  • Cesium.Cartesian2/3/4vec2/3/4
  • Cesium.Numberfloat
  • Cesium.Colorvec4
  • Cesium.Texturesampler2D
  • ...

Please check the codes in ---724116bd2a722a617a55706d533daf35 Renderer/createUniform.js , for example UniformFloatVec3 can correspond to Color and Cartesian4 and so on.

This uniforms object will eventually be merged with the system's automatic unity value ( AutomaticUniforms ) when the Context is drawn.

 Context.prototype.draw = function (/*...*/) {
  // ...
  continueDraw(this, drawCommand, shaderProgram, uniformMap);
  // ...
}

1.4. Render State Object - RenderState

The render state object must be passed to DrawCommand . The render state object type is RenderState , which is similar to ShaderProgram and provides static methods for "cached" creation:

 const renderState = RenderState.fromCache({
  depthTest: {
    enabled: true
  }
})

Even if nothing is passed: RenderState.fromCache() , a render state is returned internally.

It passes all state values that participate in WebGL rendering except rendering data. There is a detailed default list reference in RenderState The above code explicitly specifies that depth testing is to be performed.

1.5. Other constituent factors

In addition to the components 1.1 ~ 1.3, there are other options for creating drawing instructions.

① Drawn channel type - Pass

CesiumJS does not rudely traverse the Command on the frame state object and draw it. In the rendering process of Scene, in addition to generating the three Commands, there is a step to sort the Command channels.

Channel, an enumeration type, is stored in the Pass.js module. Different channels have different priorities. For example, the channel specified in 1.6 is Cesium.Pass.OPAQUE , that is, an opaque channel. In version 1.93, the order of the channels is the enumeration value:

 const Pass = {
  ENVIRONMENT: 0,
  COMPUTE: 1,
  GLOBE: 2,
  TERRAIN_CLASSIFICATION: 3,
  CESIUM_3D_TILE: 4,
  CESIUM_3D_TILE_CLASSIFICATION: 5,
  CESIUM_3D_TILE_CLASSIFICATION_IGNORE_SHOW: 6,
  OPAQUE: 7,
  TRANSLUCENT: 8,
  OVERLAY: 9,
  NUMBER_OF_PASSES: 10,
}

It can be seen that OPAQUE (opaque channel) has higher priority than TRANSLUCENT (transparent channel).

This channel may be slightly different from other graphics API channels, because you can only use this value to specify the order, rather than writing a channel yourself to composite rendering (such as ThreeJS or WebGPU).

② Drawing primitive type - WebGL drawing constant

That is to say, the topology format of the vertices in the specified VertexArray is specified by drawArrays 8aface3fcde2300a3220d566cd48cf75--- in WebGL:

 gl.drawArrays(gl.TRIANGLES, 0, 3)

This gl.TRIANGLES is the primitive type, which is a constant. Cesium is all encapsulated in the enumeration exported by the PrimitiveType.js module:

 console.log(PrimitiveType.TRIANGLES) // 4

The default is PrimitiveType.TRIANGLES , so in the 1.6 code we don't need to pass it.

③ Off-screen drawing container - Framebuffer

CesiumJS supports drawing the result to Renderbuffer , which is RTR(Render to RenderBuffer) off-screen drawing. Drawing to the rendering buffer requires a frame buffer container. CesiumJS encapsulates the frame buffer related APIs in WebGL 1/2 (strictly speaking, the APIs in WebGL are basically encapsulated).

This article only briefly mentions that about frame buffer off-screen drawing, I will introduce it later when I have the opportunity. Fake Jisi's blog has a more systematic introduction (although it is relatively old, but the idea is still there).

④ Model coordinate transformation matrix - Matrix4

Matrix4 a variable of type DrawCommand , it will eventually be passed to CesiumJS's internal uniform value: czm_model (model matrix), without needing You specify in uniform and you can use it in a vertex shader to model matrix transforms on vertices in VertexArray . See vertex shader classic MVP multiplication in 1.6.

⑤ Others

  • cull/occlude: frustum culling + horizon culling combo, Boolean
  • orientedBoundingBox/boundingVolume: bounding box
  • count: number, how many points to draw when WebGL draws
  • offset: number, the offset from which to start using vertex data when WebGL draws
  • instanceCount: number, instance drawing related
  • castShadows/receiveShadows: Boolean, shadow related
  • pickId: string, if not defined, the depth data will be used in the drawing of the Pick channel; if defined, it will be converted to pick id in GLSL
  • ...

You can find the corresponding fields in DrawCommand , and you can set them as needed.

1.6. Let's practice a solid color triangle

Everything is ready, directly rub a triangle drawing instruction StaticTrianglePrimitive , in order to facilitate use in the official sandbox, I added a namespace to the official API:

 const modelCenter = Cesium.Cartesian3.fromDegrees(112, 23, 0)
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(modelCenter)

const vertexShaderText = `attribute vec3 position;
void main() {
  gl_Position = czm_projection * czm_view * czm_model * vec4(position, 1.0);
}`
const fragmentShaderText = `uniform vec3 u_color;
void main(){
  gl_FragColor = vec4(u_color, 1.0);
}`

const createCommand = (frameState, matrix) => {
  const attributeLocations = {
    "position": 0,
  }
  const uniformMap = {
    u_color() {
      return Cesium.Color.HONEYDEW
    },
  }
  const positionBuffer = Cesium.Buffer.createVertexBuffer({
    usage: Cesium.BufferUsage.STATIC_DRAW,
    typedArray: new Float32Array([
      10000, 50000, 5000,
      -20000, -10000, 5000,
      50000, -30000, 5000,
    ]),
    context: frameState.context,
  })
  const vertexArray = new Cesium.VertexArray({
    context: frameState.context,
    attributes: [{
      index: 0, // 等于 attributeLocations['position']
      vertexBuffer: positionBuffer,
      componentsPerAttribute: 3,
      componentDatatype: Cesium.ComponentDatatype.FLOAT
    }]
  })
  const program = Cesium.ShaderProgram.fromCache({
    context: frameState.context,
    vertexShaderSource: vertexShaderText,
    fragmentShaderSource: fragmentShaderText,
    attributeLocations: attributeLocations,
  })
  const renderState = Cesium.RenderState.fromCache({
    depthTest: {
      enabled: true
    }
  })
  return new Cesium.DrawCommand({
    modelMatrix: matrix,
    vertexArray: vertexArray,
    shaderProgram: program,
    uniformMap: uniformMap,
    renderState: renderState,
    pass: Cesium.Pass.OPAQUE,
  })
}

/* ----- See Here ↓ ------ */

class StaticTrianglePrimitive {
  /**
   * @param {Matrix4} modelMatrix matrix to WorldCoordinateSystem
   */
  constructor(modelMatrix) {
    this._modelMatrix = modelMatrix
  }
  
  /**
   * @param {FrameState} frameState
   */
  update(frameState) {
    const command = createCommand(frameState, this._modelMatrix)
    frameState.commandList.push(command)
  }
}

// try!
const viewer = new Cesium.Viewer('cesiumContainer', {
  contextOptions: {
    requestWebgl2: true
  }
})
viewer.scene.globe.depthTestAgainstTerrain = true
viewer.scene.primitives.add(new StaticTrianglePrimitive(modelMatrix))

The displayed effect is a white-green triangle:

image-20220428150841888.png

The picture shows the Greater Bay Area, because the ENU coordinate center I set is near the Greater Bay Area. The height of the triangle is set to 5000 meters by me.

2. Meaning - Custom Primitive (PrimitiveLike)

If there is an object or a function that returns a drawable DrawCommand , then just pass the returned instruction object to FrameState , you can combine the above data and The drawing logic is shown.

If you think about it, there are actually quite a few objects capable of creating DrawCommand . There Primitive , BillboardCollection , SkyAtmosphere , SkyBox , Sun , Model , etc. ( Models on 3DTiles tiles are drawn by Model ).

I'll just come to the conclusion here:

  • With the function of creating DrawCommand , whether it is a function or an object, it can directly participate in the bottom drawing of Cesium;
  • The prototype chain has a class with update method, and the update method accepts a FrameState object, and adds DrawCommand to this frame state object during execution DrawCommand , you can add it to scene.primitives this PrimitiveCollection .

The former has a specific API, namely Globe under GlobeSurfaceTileProvider (used by QuadtreePrimitive ) to create DrawCommand ; .

With precise control DrawCommand , you can do the drawing you want in the Cesium scene.

click till

DrawCommand is the last data encapsulation before the CesiumJS renderer, which is followed by the distribution, binding and execution of the resources on these instruction objects. If readers are interested, they can also study ClearCommand and ComputeCommand , and maybe they will write it later, but this article will stop~

3. References


岭南灯火
83 声望64 粉丝

一介草民


引用和评论

0 条评论