Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Per pixel lighting support in Metal #1551

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

colincornaby
Copy link
Contributor

@colincornaby colincornaby commented Jan 8, 2024

This PR adds Metal per pixel lighting support to the shaders - controlled by a preprocessor flag. Per pixel lighting defaults to off in since it's an alternative rendering mode that does not match the original engine. While per pixel lighting seems correct - it should be regarded as experimental. Per pixel shading enables a shader variant for the "fixed" pipeline shader. This shader variant is controlled by a shader compile time flag. However - this flag is a function constant which means evaluation of it can be deferred to when the shader is compiled from Metal byte code into vender specific assembly. I.E. - Metal can wait to apply the flag until the final runtime shader compilation.

Metal was originally ported from the OpenGL fork which used per pixel lighting. Metal switched back to per vertex for several reasons:

  • The fragment shader function was trying to emulate the Direct3D fixed function pipeline - and because of high register usage was running into a lot of register overflows. The additional registers required by per pixel rendering was causing extreme performance issues. The shader has moved most of this register usage into compile time constants - so there are enough registers free now to do per pixel lighting.
  • Most of Uru uses lower complexity meshes or light maps. Bump mapping was never in Uru. So the benefits of per pixel lighting were less - and per pixel lighting was deprioritized.

Please look over this closely - I want to make sure the per vertex path has remained unchanged. Not in a rush to get this in.

@@ -445,7 +472,7 @@ fragment half4 pipelineFragmentShader(ColorInOut in [[stage_in]],
}
}

currentColor = half4(in.vtxColor.rgb, 1.0h) * currentColor;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a good time to ask about this one. After blending the layers - the shader again multiplies by the vertex +lighting color. This was carried over from the original GL shaders. I know if this isn't here the output is wrong. But I'm not sure why.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skimming through the code, my guess is this:
currentColor starts out set to the lighting colour with no material values, but then on the first layer is overwritten to the layer colours of the first layer (in blendFirst) with subsequent layers blending into that. Since we've lost the lighting colours at that point, we need to multiply them back in.

@colincornaby
Copy link
Contributor Author

I brought this branch up to date with master - and now it's acting strangely. For example - it's hitting asserts while trying to load the server.ini. I'll dig a bit and see whats going on. I'm pretty sure this behavior is newly unique to this branch. Last time I was in here it was working.

@colincornaby
Copy link
Contributor Author

The issue I was befuddled by is addressed in #1591.

@dpogue
Copy link
Member

dpogue commented Jun 2, 2024

FWIW I'm pretty sure OpenGL is also doing per-vertex lighting to match DirectX.

I might have been doing that in the fragment shader at one point while I was getting it set up, but it's in the vertex shader in the current version of the GL branch.

@colincornaby
Copy link
Contributor Author

Re-stating discussions elsewhere here for the status on this branch.

  • While evaluating the lighting pipeline - it was found Metal is not properly handling the bump maps.
  • Plasma calls them "bump maps" but they actually seem to be normal maps.
  • Normal maps typically require per pixel lighting. In the D3D renderer there is a algorithm that attempts to bake the lighting algorithm into the texturing pipeline.
  • This algorithm has significant issues - but would have been a reasonable approximation in the early 2000s.

Relative to this PR:

  • We're not going to reimplement Cyan's algorithm in Metal. We have the ability to do proper normal mapping on Metal hardware.
  • We will not ship per pixel lighting by default - but meshes can opt into it.
  • Meshes with normal mapping will be opted into per pixel lighting. A normal mapping path in the shaders will be implemented in a future PR.
  • The Metal pipeline will render normal maps differently than Windows - but the quality should be much higher.

We'll want to move ahead with this PR to get normal mapping into Metal.

@colincornaby
Copy link
Contributor Author

@dpogue - Checking back in to see if you have any thoughts here. We'll need this for bump mapping (even if we don't turn it on for everything.) Not high priority, just didn't want it to get lost.

Copy link
Member

@dpogue dpogue left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I think this is okay. Admittedly I only skimmed the actual shader code implementation details.

I think we should do the work now to refactor this a bit so that it can be turned on/off dynamically per mesh, even if we don't actually hook up the dynamic choice yet.

Otherwise, just a few code style issues.

Comment on lines 156 to 159
if (PLASMA_PER_PIXEL_LIGHTING)
{
fDevice->CurrentRenderCommandEncoder()->setFragmentBytes(&fDevice->fPipeline->fCurrentRenderPassMaterialLighting, sizeof(plMaterialLightingDescriptor), FragmentShaderArgumentMaterialLighting);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't match our coding style, but we probably also want to make this controlled by a pipeline value rather than a define (even if we just hardcode that pipeline value to the value of the define for now)

Comment on lines 164 to 169
if (PLASMA_PER_PIXEL_LIGHTING)
{
fDevice->CurrentRenderCommandEncoder()->setFragmentBytes(lights, sizeof(plMetalLights), FragmentShaderArgumentLights);
} else {
fDevice->CurrentRenderCommandEncoder()->setVertexBytes(lights, sizeof(plMetalLights), VertexShaderArgumentLights);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't match our coding style, but we probably also want to make this controlled by a pipeline value rather than a define (even if we just hardcode that pipeline value to the value of the define for now)

Comment on lines 254 to 262
if (perPixelLighting)
{
// send the world pos on to the pixel shader for lighting
out.worldPos = position;
out.normal = Ndirection;
}

if (perPixelLighting)
{
out.vtxColor = inColor;
} else {
out.vtxColor = calcLitMaterialColor(lights, inColor, materialLighting, position, Ndirection);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how strict our coding style has been within Metal shaders, but the opening brace should be on the same line as the if

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Metal Shaders are C++ - I'm assuming we should apply the same coding style.

Comment on lines 113 to 114
bool perPixelLighting = PLASMA_PER_PIXEL_LIGHTING;
constants->setConstantValue(&perPixelLighting, MTL::DataTypeBool, FunctionConstantPerPixelLighting);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: move the calculation of this to the pipeline so that it can (later) be dynamic per mesh

Copy link
Member

@Hoikas Hoikas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs code style cleanups

@colincornaby
Copy link
Contributor Author

With the latest changes I added some better detection of redundant bindings. I can see the redundant bindings in the Metal debugger, and it's been on my list of things to address. Unnecessary draw commands create a larger buffer to send to the GPU - so they should be avoided.

However - right now I'm using memcmp to compare struct values. I'd rather use a bool that marks when something is dirty and needs to be sent to the GPU. I've left notes behind in the code and will continue improving this in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants