# Below is a very slightly adjusted ps1ify script
# intended for use with Chulip models ripped using
# scurest's PCSX2 3D Screenshot Build:
# https://github.com/scurest/pcsx2/releases/tag/latest-3d-screenshot

# If you're using this on models you ripped yourself,
# add "transparent" to the name of any material that is
# erroneously transparent after running the script the first time

# -------

# Create PlayStation 1 shaders for PS1 model rips.
# Usage: Run script with imported PS1 models selected.
# Tested with: Blender 3.6, 4.0, 4.1

import bpy

def ps1ify_material(mesh, material):
    nodes = [] if not material.node_tree else material.node_tree.nodes
    img_nodes = [n for n in nodes if n.type == 'TEX_IMAGE' and n.image]
    image = img_nodes[0].image if img_nodes else None

    vertex_color = '' if mesh.color_attributes else None

    semitransparent = 'transparent' in material.name.lower()

    setup_ps1_material(
        material,
        image=image,
        vertex_color=vertex_color,
        use_semitransparent_mode0=semitransparent,
    )
    material.use_backface_culling = False
    material.blend_method = 'HASHED' if not semitransparent else 'BLEND'


# Overview of PS1 fragment processing (AIUI):
#
#  * Polygons may be textured/untextured and colored/uncolored.
#  * When using both texture and vertex color, they are combined
#    with 2 * vertexColor * textureColor.
#  * Textures use nearest neighbor (pixelated) filtering.
#  * Textures and vertex colors do not have alpha channels.
#    Instead...
#  * Black (#000) texels are treated as fully transparent. (We
#    assume this fact is already baked into the textures.)
#  * When semi-transparency mode 0 is enabled, the background
#    and foreground are combined with B/2+F/2, ie. blended with
#    alpha=50%. Other semi-transparency modes are not supported
#    because they aren't subsets of alpha blending.

def setup_ps1_material(
    material,
    image,
    vertex_color=None,  # vertex color attribute name ('' = default)
    use_texture_alpha=True,
    use_semitransparent_mode0=False,
):
    if not image:
        use_texture_alpha = False
    use_alpha = use_texture_alpha or use_semitransparent_mode0
    use_color = vertex_color is not None

    if not image and not use_color:
        return

    material.use_nodes = True
    nodes = material.node_tree.nodes
    links = material.node_tree.links

    nodes.clear()
    node_out = nodes.new('ShaderNodeOutputMaterial')

    # Texture / Vertex Color

    if use_color:
        node_color = nodes.new('ShaderNodeVertexColor')
        node_color.layer_name = vertex_color
    if image:
        node_img = nodes.new('ShaderNodeTexImage')
        node_img.image = image
        node_img.interpolation = 'Closest'

    if image and not use_color:
        color_socket = node_img.outputs['Color']

        node_img.location = -400, 100

    elif not image and use_color:
        color_socket = node_color.outputs['Color']

        node_color.location = -300, 80

    else:
        # Vertex color and texture color are combined with
        #
        #   outputColor = 2 * vertexColor * textureColor
        #
        # This is done in sRGB. In the Blender shader, the colors have been
        # converted to linear. The equivalent in linear is approximately
        #
        #   outputColor = 4.6 * vertexColor * textureColor

        # Multiply vertex color by 2
        node_2x = nodes.new('ShaderNodeVectorMath')
        node_2x.label = 'Multiply x 2'
        node_2x.operation = 'MULTIPLY'
        if use_semitransparent_mode0:
            node_2x.inputs[1].default_value = [12.5] * 3
        else:
            node_2x.inputs[1].default_value = [4.63262] * 3
        links.new(node_color.outputs['Color'], node_2x.inputs[0])

        # Multiply with texture color
        node_mul = nodes.new('ShaderNodeMix')
        node_mul.data_type = 'RGBA'
        node_mul.blend_type = 'MULTIPLY'
        node_mul.clamp_result = True
        node_mul.inputs[0].default_value = 1                      # Fac
        links.new(node_img.outputs['Color'], node_mul.inputs[6])  # Color A
        links.new(node_2x.outputs[0], node_mul.inputs[7])         # Color B

        color_socket = node_mul.outputs[2]

        node_mul.location = -300, 40
        node_img.location = -700, 200
        node_2x.location = -600, -200
        node_color.location = -800, -200

    # Alpha

    if not use_alpha:
        links.new(color_socket, node_out.inputs[0])

        node_out.location = 20, 20

    else:
        node_alpha = nodes.new('ShaderNodeMixShader')
        node_transp = nodes.new('ShaderNodeBsdfTransparent')
        links.new(node_transp.outputs[0], node_alpha.inputs[1])
        links.new(node_alpha.outputs[0], node_out.inputs[0])
        links.new(color_socket, node_alpha.inputs[2])

        node_alpha.location = 20, 200
        node_transp.location = -300, 400
        node_out.location = 250, 260

    if use_texture_alpha and not use_semitransparent_mode0:
        links.new(node_img.outputs['Alpha'], node_alpha.inputs[0])

    elif not use_texture_alpha and use_semitransparent_mode0:
        node_alpha.inputs[0].default_value = 0.5

    elif use_texture_alpha and use_semitransparent_mode0:
        node_alpha.inputs[0].default_value = 1
        node_img.image.alpha_mode = 'STRAIGHT'
        # Alpha = TextureAlpha * 0.5
        #node_amul = nodes.new('ShaderNodeMath')
        #node_amul.operation = 'MULTIPLY'
        #links.new(node_img.outputs['Alpha'], node_amul.inputs[0])
        #node_amul.inputs[1].default_value = 0.5
        #links.new(node_amul.outputs[0], node_alpha.inputs[0])

        #node_amul.location = -300, 250
        if not use_color:
            node_img.location = -650, 0


def set_color_management():
    # Turn off default Filmic color management, which darkens color.
    # See eg: https://blender.stackexchange.com/questions/164677
    bpy.context.scene.display_settings.display_device = 'sRGB'
    bpy.context.scene.view_settings.view_transform = 'Standard'
    bpy.context.scene.sequencer_colorspace_settings.name = 'sRGB'


if __name__ == '__main__':
    obs = bpy.context.selected_objects or bpy.data.objects
    for ob in obs:
        if ob.type != 'MESH': continue
        for mat in ob.data.materials:
            if not mat: continue
            ps1ify_material(ob.data, mat)
    set_color_management()
