shaders in game editor written by programmers just get compiled into opengl shaders.
Yes, this is exactly the case. At the end of the day, whatever you write in Unity ShaderLab or build in a shader graph ultimately gets compiled down to a standard hlsl/glsl/vulkan/metal shader program (depending on your target platform).
Parts of the shader that need to work in a consistent way to play nicely with the engine's conventions are handled in a few different ways, from least to most complex:
Built-in variables and functions: Unity provides many pre-defined uniform variables, helper functions, and structs that you can access via an #include "UnityCG.cginc"
directive.
Want the coordinate transformation to apply for the current object? That's available, pre-populated by the engine into a variable called unity_ObjectToWorld
. Want to project it through the current camera, that's UNITY_MATRIX_MVP
. Need the position and projection parameters of the current camera? They're available as _WorldSpaceCameraPos
and _ProjectionParams
. These and many more are listed here
Preprocessor macros: Some operations are a bit more complicated than a pre-defined variable or function, because the data types or steps of the process might be different depending on the platform, or the rendering settings, or whether you're in VR, etc.
For these cases, Unity provides macros that are recognized by its shader compiler, and expanded into the appropriate shader code depending on the context of the shader version being compiled.
These include things like UnityObjectToClipPos()
which handles transforming vertices (a little more efficiently than multiplying out the matrices yourself, since it's aware of the batching/instancing Unity uses internally), or LinearEyeDepth(i)
for decoding a sample from a depth map, whatever encoding it might use on the target platform/graphics settings.
Shader templates: Working consistently with lighting can be a challenge. You'd have to copy a lot of shading boilerplate into every fragment shader, and remember to call all the right macros to get the same physically-based results as the built-in ones, under all lighting scenarios, even if you only wanted to change one small detail.
To help with this, Unity introduces what it calls a "Surface Shader". Here, you don't write a vertex or fragment program, but a "surface program," whose job is to populate a data structure with the albedo, metalness, roughness, emission, and normal of the shaded point — but not worry about any of the shading. Under the hood, when Unity compiles a vertex/fragment shader from this, it inserts your surface logic into the appropriate place in its built-in lighting shader template for each lighting configuration needed.
This lets you get consistent lighting & shading without worrying about that complexity, and your code describing the surface properties is portable between different lighting and shading models.
All that applies to shaders that you write in text. For node-graph shaders, the situation is much the same, except all the tricks above just manifest as nodes. Nodes that access useful pre-defined uniforms. Function nodes that compile down to different code depending on the platform/scenario. Shader root nodes represent different template shaders, that expose the parameters you're allowed to modify, and compile that code into the boilerplate for the parts that are standardized.