Aperture Temporary Migration Guide
Aperture is a new pipeline for Iris, set to be released with Iris 2.0.
- It uses pipeline code written by shader developers in TypeScript, allowing for fully customizable packs.
- It has a mod API that allows for mods to apply custom vertex formats, vertex transformations, and (basic) fragment warping without interfering with shader developers.
- It has no limits on the number of textures, buffers, or programs you can have, how complex they can be, or how they are running.
- It has a new shadow mapping system based on cascaded shadow maps, allowing shadow maps that reach far beyond what Optifine packs can without distortion.
- Packs written on Aperture use a modern version of OpenGL that can be translated to other API’s in the future.
This serves as temporary documentation whilst Aperture is being developed.
pack.ts
pack.ts
is where the shaderpack is configured. All textures and programs are registered from here. When instantiating objects like textures and programs, options for them can be configured using builder syntax. For example, to set the format of a texture, I would call .format(<FORMAT>)
on it. Once an object is built, the .build()
function must be called. For example:
To see a list of builder functions that can be called on an object (and what can be passed to them), you can go to the type definition in iris.d.ts
.
Registering Textures
Textures are registered in the setupShader
function. They are instantiated by creating a new Texture
object, or alternatively:
RawTexture
(which reads from a binary file)ArrayTexture
(required for the shadow map)PNGTexture
(guess what this does)
Registering Programs
Shader programs are registered in the setupShader
function with the registerShader
function, which takes in one of the following
ObjectShader
(‘gbuffers’ style)Composite
(fullscreen pass)Compute
ObjectShader
s take a programUsage
, these are predefined for you, for example TERRAIN_SOLID
.
Composite
and Compute
shaders require a Stage
argument passed to registerShader
before the shader object itself. This defines when the shader runs - for example Stage.POST_RENDER
is equivalent to a composite
pass in the Optifine format.
To add a vertex and fragment shader to your shader object, you can call .vertex(<PATH_TO_VERTEX>)
and .fragment(<PATH_TO_FRAGMENT>)
.
To choose which buffers the fragment shader writes to, use .target(<index>, <texture>)
- index
is the index in your layout(location = <index>)
definition. This essentially replaces RENDERTARGETS
.
Object Shaders
Object shaders require special handling to allow for mod support
Vertex
Vertex shaders should define two functions:
void iris_emitVertex(inout VertexData data)
Set data.clipPos
as you would gl_Position
. Usually this can just be iris_projectionMatrix * iris_modelViewMatrix * data.modelPos
void iris_sendParameters(VertexData data)
This is where you can write to your out
declarations.
Both of these functions make use of the VertexData
struct.
Fragment
Fragment shaders are a lot simpler, you just define void iris_emitFragment()
instead of void main()
.
Composite Shaders
Vertex
Since Aperture uses the core profile, ftransform()
is not patched. Instead, the position of the vertex and the UV must be manually calculated as follows.
Buffers
The depth textures are mainDepthTex
and solidDepthTex
, replacing depthtex0
and depthtex1
.
Uniforms
A list of available uniforms is printed to the console on Iris startup in the form of the structs they are stored in. As an example of how to access these values, cameraPosition
is now the pos
member of the CameraData
struct, and to access it, we would use ap.camera.pos
. The same pattern can be applied to any struct.
Shadows
Iris makes use of cascaded shadow maps, which means everything is a lot more complicated.
Setting Up the Shadow Pass
You do not need to declare the shadow map, as it is automatically defined. If you want any shadowcolor
style buffers, declare them as an ArrayTexture
.
Sampling the Shadow Map
Aperture defines two shadow maps.
shadowMap
(equivalent toshadowtex0
)solidShadowMap
(equivalent toshadowtex1
)
Both of these should be defined as sampler2DArray
s. To sample them, you pass the cascade as the z
component of your coordinate, i.e:
Cascade 0 is the smallest, containing only a small area around the player, and cascade 3 is the largest.
Bear in mind that the shadow projection is also an array, because each cascade has a different projection. As such, to sample the shadow map, you must loop through each projection until your position is inside the frustum.
Hardware Filtering
If you want hardware filtering, you can instead use a sampler2DArrayShadow
and suffix filtered
to the sampler names (i.e shadowMapFiltered
. Due to reasons known only to the developers over at Khronos, the cascade is still the z
component of the coordinate you pass in, so it becomes
Block IDs
The VertexData
object passed to iris_sendParameters
has a blockId
attribute. The following functions can be used on the ID:
The metadata comes in the form of an 18 bit uint with the following structure