mirror of
https://github.com/ApfelTeeSaft/lightspeed64.git
synced 2026-08-26 19:33:24 +00:00
* [SM64/F3D] Use WriteDifferingAndRevert for bleed Write all logic remains unaffected, obviously. A new dict was added to map each mode cmd to its default. For the actual bleed part, we fIrst need to add geo mode reverts of the last material to the geo mode of the current material, since the current material may not set them now. For othermodes we do something different, get all reverts and add any that doesn´t get set again by the current material ignoring bleeding. For the geo mode revert, we keep track of one geo mode command for set and clear, updating it as we go. For othermodes we keep track of all of the command types and then revert them using the new dict. * remove this * scut sabotage * Port over #462 We cannot just assume that othermodeh or othermodel are always setting the same bits, so instead we write the diff of the current cmd to the one already in the reset dict, this means that if a materials sets rendermode and another sets alpha compare (for example) it will still revert the rendermode because those bits will be carried over in the reset dict * make write different optional, as it can lead to bigger dls * clean up sm64 repo settings * im gonna have to rework a bunch of othermode stuff i actually dispise fast64 so much * RendermodeBlender class * Update f3d_bleed.py * i just want to be done with this * [Repo Settings] Only set if different Fixes lag at start up from draw layers being set unnecessarily. Some clean up that I already did in #461 * Update utility.py * is_rendermode_cmd to def * fix rendermode * return RendermodeBlender instead of temp list * instead of using a revert, set when no render mode * make rendermode command hashable, check for default * pick appropriate geo cmd, make geo cmds use sets makes dls just a bit cleaner and makes ex1 work correctly * implement render mode reset fix * Update f3d_bleed.py * undo dumb change * respect write method * only revert as needed, i feel like a god * make switchs work super well * fix setting rendermode * remove repeated resets in switch options and add options for reverts in bones * use last_mat by default * comment out occlusion plane code to show bleed indep * functions already work * fix load othermodes these work more like write diffs, so we do that * someone forgot * fix prev node issue * create our own pipe syncs * Update f3d_bleed.py * fix disgusting broken late night code * Revert "fix disgusting broken late night code" This reverts commit 9ece1511595678a551fae02f906a998fbde1ffba. * Update sm64_geolayout_classes.py * something is wrong but i cant figure it out * dont remove lighting * fix DPSetRenderMode * fix write all rendermode * fix this bug once and for all * write all get_flags impl * get_flags * forgot to do diff in reverts in write all * add back prim depth, accidentally removed * comments (not ready) * not a union * not a tuple Co-authored-by: Dragorn421 <[email protected]> * proper formatting Co-authored-by: Dragorn421 <[email protected]> * use load sync * oopsie dasie * use pipe syncs again --------- Co-authored-by: Dragorn421 <[email protected]>
1374 lines
56 KiB
Python
1374 lines
56 KiB
Python
from typing import Union, Optional
|
|
from dataclasses import dataclass, field
|
|
import bpy
|
|
from math import ceil, floor
|
|
|
|
from .f3d_enums import *
|
|
from .f3d_material import (
|
|
all_combiner_uses,
|
|
getTmemWordUsage,
|
|
texBitSizeF3D,
|
|
texFormatOf,
|
|
TextureProperty,
|
|
F3DMaterialProperty,
|
|
isTexturePointSampled,
|
|
)
|
|
from .f3d_gbi import *
|
|
from .f3d_gbi import _DPLoadTextureBlock
|
|
from .flipbook import TextureFlipbook
|
|
|
|
from ..utility import *
|
|
|
|
|
|
def UVtoSTLarge(obj, loopIndex, uv_data, texDimensions):
|
|
uv = uv_data[loopIndex].uv.copy()
|
|
uv[1] = 1 - uv[1]
|
|
loopUV = uv.freeze()
|
|
|
|
# Represent the -0.5 texel offset in the UVs themselves in clamping mode
|
|
# if desired, rather than here at export
|
|
pixelOffset = 0
|
|
return [
|
|
convertFloatToFixed16(loopUV[0] * texDimensions[0] - pixelOffset) / 32,
|
|
convertFloatToFixed16(loopUV[1] * texDimensions[1] - pixelOffset) / 32,
|
|
]
|
|
|
|
|
|
class TileLoad:
|
|
def __init__(self, material, fMaterial, texDimensions):
|
|
self.sl = self.tl = 1000000 # above any actual value
|
|
self.sh = self.th = -1 # below any actual value
|
|
|
|
self.texFormat = fMaterial.largeTexFmt
|
|
self.is4bit = texBitSizeInt[self.texFormat] == 4
|
|
self.tmemWordsAvail = fMaterial.largeTexWords
|
|
self.texDimensions = texDimensions
|
|
self.materialName = material.name
|
|
self.isPointSampled = isTexturePointSampled(material)
|
|
self.largeEdges = material.f3d_mat.large_edges
|
|
|
|
self.faces = []
|
|
self.offsets = []
|
|
|
|
def getLow(self, value, field):
|
|
value = int(floor(value))
|
|
if self.largeEdges == "Clamp":
|
|
value = min(max(value, 0), self.texDimensions[field] - 1)
|
|
if self.is4bit and field == 0:
|
|
# Must start on an even texel (round down)
|
|
value &= ~1
|
|
return value
|
|
|
|
def getHigh(self, value, field):
|
|
value = int(ceil(value)) - (1 if self.isPointSampled else 0)
|
|
if self.largeEdges == "Clamp":
|
|
value = min(max(value, 0), self.texDimensions[field] - 1)
|
|
if self.is4bit and field == 0:
|
|
# Must end on an odd texel (round up)
|
|
value |= 1
|
|
return value
|
|
|
|
def fixRegion(self, sl, sh, tl, th):
|
|
assert sl <= sh and tl <= th
|
|
soffset = int(floor(sl / self.texDimensions[0])) * self.texDimensions[0]
|
|
toffset = int(floor(tl / self.texDimensions[1])) * self.texDimensions[1]
|
|
sl -= soffset
|
|
sh -= soffset
|
|
tl -= toffset
|
|
th -= toffset
|
|
assert 0 <= sl < self.texDimensions[0] and 0 <= tl < self.texDimensions[1]
|
|
ret = True
|
|
if sh >= 1024 or th >= 1024:
|
|
ret = False
|
|
if sh >= self.texDimensions[0]:
|
|
# Load wraps in S. Load must start a multiple of a TMEM word from
|
|
# the end of the texture, in order for the second load (beginning of
|
|
# image) to start at a whole word.
|
|
texelsPerWord = 64 // texBitSizeInt[self.texFormat]
|
|
if texelsPerWord > self.texDimensions[0]:
|
|
raise PluginError(
|
|
f"In large texture material {self.materialName}:"
|
|
+ f" large texture must be at least {texelsPerWord} wide."
|
|
)
|
|
sl -= self.texDimensions[0]
|
|
sl = int(floor(sl / texelsPerWord)) * texelsPerWord
|
|
sl += self.texDimensions[0]
|
|
if th >= self.texDimensions[1]:
|
|
# Load wraps in T. Load must start a multiple of 2 texture rows from
|
|
# the end of the texture, in order for the second load to have the
|
|
# same odd/even row parity as the first (because texels are
|
|
# interleaved in TMEM every other row).
|
|
tl -= self.texDimensions[1]
|
|
tl = int(floor(tl / 2.0)) * 2
|
|
tl += self.texDimensions[1]
|
|
tmemUsage = getTmemWordUsage(self.texFormat, sh - sl + 1, th - tl + 1)
|
|
if tmemUsage > self.tmemWordsAvail:
|
|
ret = False
|
|
return ret, sl, sh, tl, th, soffset, toffset
|
|
|
|
def initWithFace(self, obj, face):
|
|
uv_data = obj.data.uv_layers["UVMap"].data
|
|
faceUVs = [UVtoSTLarge(obj, loopIndex, uv_data, self.texDimensions) for loopIndex in face.loops]
|
|
if len(faceUVs) == 0:
|
|
return True
|
|
|
|
for point in faceUVs:
|
|
self.sl = min(self.sl, self.getLow(point[0], 0))
|
|
self.sh = max(self.sh, self.getHigh(point[0], 0))
|
|
self.tl = min(self.tl, self.getLow(point[1], 1))
|
|
self.th = max(self.th, self.getHigh(point[1], 1))
|
|
|
|
ret, self.sl, self.sh, self.tl, self.th, soffset, toffset = self.fixRegion(self.sl, self.sh, self.tl, self.th)
|
|
if not ret:
|
|
if self.sh >= 1024 or self.th >= 1024:
|
|
raise PluginError(
|
|
f"Large texture material {self.materialName} has a face that needs"
|
|
+ f" to cover texels {self.sl}-{self.sh} x {self.tl}-{self.th}"
|
|
+ f" (image dims are {self.texDimensions}), but image space"
|
|
+ f" only goes up to 1024 so this cannot be represented."
|
|
)
|
|
else:
|
|
raise PluginError(
|
|
f"Large texture material {self.materialName} has a face that needs"
|
|
+ f" to cover texels {self.sl}-{self.sh} x {self.tl}-{self.th}"
|
|
+ f" ({self.sh-self.sl+1} x {self.th-self.tl+1} texels) "
|
|
+ f"in format {self.texFormat}, which can't fit in TMEM."
|
|
)
|
|
self.faces.append(face)
|
|
self.offsets.append((soffset, toffset))
|
|
|
|
def trySubsume(self, other):
|
|
"""
|
|
Attempts to enlarge the self TileLoad to cover both itself and the other
|
|
TileLoad. If this succeeds, self is modified and True is returned. If it
|
|
fails (because it would be too large or the other constraints from
|
|
fixRegion would be violated), self is not modified and False is returned.
|
|
A large texture mesh is built by, for each triangle, trying to subsume
|
|
it into each of the existing loads. If it succeeds on one of them, it
|
|
moves on to the next triangle. If it fails on all of them, a new load is
|
|
created for that triangle and added to the list.
|
|
"""
|
|
# Could do fancier logic checking across borders, for example if we have
|
|
# one loading 60-68 (size 64) and another 0-8, that could be merged to
|
|
# one load 60-72. But this is likely to be uncommon and won't be generated
|
|
# by the operator.
|
|
new_sl = min(self.sl, other.sl)
|
|
new_sh = max(self.sh, other.sh)
|
|
new_tl = min(self.tl, other.tl)
|
|
new_th = max(self.th, other.th)
|
|
ret, new_sl, new_sh, new_tl, new_th, soffset, toffset = self.fixRegion(new_sl, new_sh, new_tl, new_th)
|
|
if not ret:
|
|
return False
|
|
self.sl, self.sh, self.tl, self.th = new_sl, new_sh, new_tl, new_th
|
|
self.faces.extend(other.faces)
|
|
self.offsets.extend(other.offsets)
|
|
return True
|
|
|
|
|
|
def maybeSaveSingleLargeTextureSetup(
|
|
i: int,
|
|
fMaterial: FMaterial,
|
|
fModel: FModel,
|
|
fImage: FImage,
|
|
gfxOut: GfxList,
|
|
texProp: TextureProperty,
|
|
texDimensions: tuple[int, int],
|
|
tileSettings: TileLoad,
|
|
curImgSet: Optional[int],
|
|
curTileLines: list[int],
|
|
):
|
|
"""
|
|
Checks whether a particular texture is large and if so, writes the loads for
|
|
that large texture. "maybe" is to bring the if statement into the function
|
|
instead of checking whether the texture is large before calling it.
|
|
"""
|
|
if fMaterial.isTexLarge[i]:
|
|
wrapS = tileSettings.sh >= texDimensions[0]
|
|
wrapT = tileSettings.th >= texDimensions[1]
|
|
assert 0 <= tileSettings.sl < texDimensions[0]
|
|
assert 0 <= tileSettings.tl < texDimensions[1]
|
|
siz = texBitSizeF3D[texProp.tex_format]
|
|
line = getTileLine(fImage, tileSettings.sl, tileSettings.sh, siz, fModel.f3d)
|
|
tmem = fMaterial.largeTexAddr[i]
|
|
if wrapS or wrapT:
|
|
fmt = texFormatOf[texProp.tex_format]
|
|
texelsPerWord = 64 // texBitSizeInt[texProp.tex_format]
|
|
wid = texDimensions[0]
|
|
is4bit = siz == "G_IM_SIZ_4b"
|
|
if is4bit:
|
|
siz = "G_IM_SIZ_8b"
|
|
wid >>= 1
|
|
assert (tileSettings.sl & 1) == 0
|
|
assert (tileSettings.sh & 1) == 1
|
|
# TL, TH is always * 4 because tile values are 10.2 fixed.
|
|
# SL, SH is * 2 for 4 bit and * 4 otherwise, because actually loading
|
|
# 8 bit pairs of texels. Also written using f3d.G_TEXTURE_IMAGE_FRAC.
|
|
sm = 2 if is4bit else 4
|
|
nocm = ("G_TX_WRAP", "G_TX_NOMIRROR")
|
|
if curImgSet != i:
|
|
gfxOut.commands.append(DPSetTextureImage(fmt, siz, wid, fImage))
|
|
|
|
def loadOneOrTwoS(tmemBase, tidxBase, TL, TH):
|
|
if line != curTileLines[tidxBase]:
|
|
gfxOut.commands.append(DPSetTile(fmt, siz, line, tmemBase, tidxBase, 0, nocm, 0, 0, nocm, 0, 0))
|
|
curTileLines[tidxBase] = line
|
|
if wrapS:
|
|
# Break up at the wrap boundary into two tile loads.
|
|
# The first load must occupy a whole number of lines.
|
|
assert (texDimensions[0] - tileSettings.sl) % texelsPerWord == 0
|
|
sLineOfs = (texDimensions[0] - tileSettings.sl) // texelsPerWord
|
|
gfxOut.commands.append(
|
|
DPLoadTile(tidxBase, tileSettings.sl * sm, TL * 4, (texDimensions[0] - 1) * sm, TH * 4)
|
|
)
|
|
gfxOut.commands.append(
|
|
DPSetTile(fmt, siz, line, tmemBase + sLineOfs, tidxBase - 1, 0, nocm, 0, 0, nocm, 0, 0)
|
|
)
|
|
curTileLines[tidxBase - 1] = -1
|
|
gfxOut.commands.append(
|
|
DPLoadTile(tidxBase - 1, 0, TL * 4, (tileSettings.sh - texDimensions[0]) * sm, TH * 4)
|
|
)
|
|
else:
|
|
gfxOut.commands.append(
|
|
DPLoadTile(tidxBase, tileSettings.sl * sm, TL * 4, tileSettings.sh * sm, TH * 4)
|
|
)
|
|
|
|
if wrapT:
|
|
# Break up at the wrap boundary into two loads.
|
|
# The first load must be even in size (even number of texture rows).
|
|
assert (texDimensions[1] - tileSettings.tl) % 2 == 0
|
|
tLineOfs = line * (texDimensions[1] - tileSettings.tl)
|
|
loadOneOrTwoS(tmem, 7, tileSettings.tl, texDimensions[1] - 1)
|
|
loadOneOrTwoS(tmem + tLineOfs, 5, 0, tileSettings.th - texDimensions[1])
|
|
else:
|
|
loadOneOrTwoS(tmem, 7, tileSettings.tl, tileSettings.th)
|
|
if fMaterial.isTexLarge[i ^ 1]:
|
|
# May reuse any of the above tiles for the other large texture.
|
|
gfxOut.commands.append(DPTileSync())
|
|
else:
|
|
saveTextureLoadOnly(
|
|
fImage,
|
|
gfxOut,
|
|
texProp,
|
|
tileSettings,
|
|
7 - i,
|
|
tmem,
|
|
fModel.f3d,
|
|
curImgSet == i,
|
|
line == curTileLines[7 - i],
|
|
)
|
|
curTileLines[7 - i] = line
|
|
curImgSet = i
|
|
saveTextureTile(
|
|
fImage,
|
|
fMaterial,
|
|
gfxOut,
|
|
texProp,
|
|
tileSettings,
|
|
i,
|
|
tmem,
|
|
fMaterial.texPaletteIndex[i],
|
|
fModel.f3d,
|
|
line == curTileLines[i],
|
|
)
|
|
curTileLines[i] = line
|
|
return curImgSet
|
|
|
|
|
|
# Functions for texture and palette definitions
|
|
|
|
|
|
def getTextureNamesFromBasename(baseName: str, texOrPalFormat: str, parent: Union[FModel, FTexRect], isPalette: bool):
|
|
suffix = getTextureSuffixFromFormat(texOrPalFormat)
|
|
imageName = parent.name + "_" + baseName + "_"
|
|
if isPalette:
|
|
imageName += "pal_"
|
|
imageName += suffix
|
|
imageName = checkDuplicateTextureName(parent, toAlnum(imageName))
|
|
filename = baseName + (f"" if (baseName.endswith(suffix)) else f".{suffix}") + (".pal" if isPalette else ".inc.c")
|
|
return imageName, filename
|
|
|
|
|
|
def getImageName(image: bpy.types.Image):
|
|
if image is None:
|
|
raise PluginError("No image set in material!")
|
|
elif image.filepath == "":
|
|
return image.name
|
|
else:
|
|
return getNameFromPath(image.filepath, True)
|
|
|
|
|
|
def getTextureNamesFromImage(image: bpy.types.Image, texFormat: str, parent: Union[FModel, FTexRect]):
|
|
return getTextureNamesFromBasename(getImageName(image), texFormat, parent, False)
|
|
|
|
|
|
def getTextureNamesFromProp(texProp: TextureProperty, parent: Union[FModel, FTexRect]):
|
|
if texProp.use_tex_reference:
|
|
raise PluginError("Internal error, invalid use of getTextureNamesFromProp")
|
|
return getTextureNamesFromImage(texProp.tex, texProp.tex_format, parent)
|
|
|
|
|
|
def checkDuplicateTextureName(parent: Union[FModel, FTexRect], name):
|
|
names = []
|
|
for info, texture in parent.textures.items():
|
|
names.append(texture.name)
|
|
while name in names:
|
|
name = name + "_copy"
|
|
return name
|
|
|
|
|
|
def saveOrGetPaletteDefinition(
|
|
fMaterial: FMaterial,
|
|
parent: Union[FModel, FTexRect],
|
|
texProp: TextureProperty,
|
|
isPalRef: bool,
|
|
images: list[bpy.types.Image],
|
|
palBaseName: str,
|
|
palLen: int,
|
|
) -> tuple[FPaletteKey, FImage]:
|
|
texFmt = texProp.tex_format
|
|
palFmt = texProp.ci_format
|
|
palFormat = texFormatOf[palFmt]
|
|
paletteKey = FPaletteKey(palFmt, images)
|
|
|
|
if isPalRef:
|
|
fPalette = FImage(texProp.pal_reference, None, None, 1, palLen, None)
|
|
return paletteKey, fPalette
|
|
|
|
# If palette already loaded, return that data.
|
|
fPalette = parent.getTextureAndHandleShared(paletteKey)
|
|
if fPalette is not None:
|
|
# print(f"Palette already exists")
|
|
return paletteKey, fPalette
|
|
|
|
paletteName, filename = getTextureNamesFromBasename(palBaseName, palFmt, parent, True)
|
|
fPalette = FImage(paletteName, palFormat, "G_IM_SIZ_16b", 1, palLen, filename)
|
|
|
|
parent.addTexture(paletteKey, fPalette, fMaterial)
|
|
return paletteKey, fPalette
|
|
|
|
|
|
def saveOrGetTextureDefinition(
|
|
fMaterial: FMaterial,
|
|
parent: Union[FModel, FTexRect],
|
|
texProp: TextureProperty,
|
|
images: list[bpy.types.Image],
|
|
isLarge: bool,
|
|
) -> tuple[FImageKey, FImage]:
|
|
image = texProp.tex
|
|
texFmt = texProp.tex_format
|
|
texFormat = texFormatOf[texFmt]
|
|
bitSize = texBitSizeF3D[texFmt]
|
|
imageKey = getImageKey(texProp, images)
|
|
|
|
if texProp.use_tex_reference:
|
|
width, height = texProp.tex_reference_size
|
|
fImage = FImage(texProp.tex_reference, None, None, width, height, None)
|
|
return imageKey, fImage
|
|
|
|
# If image already loaded, return that data.
|
|
fImage = parent.getTextureAndHandleShared(imageKey)
|
|
if fImage is not None:
|
|
# print(f"Image already exists")
|
|
return imageKey, fImage
|
|
|
|
imageName, filename = getTextureNamesFromProp(texProp, parent)
|
|
fImage = FImage(imageName, texFormat, bitSize, image.size[0], image.size[1], filename)
|
|
fImage.isLargeTexture = isLarge
|
|
|
|
parent.addTexture(imageKey, fImage, fMaterial)
|
|
return imageKey, fImage
|
|
|
|
|
|
@dataclass
|
|
class TexInfo:
|
|
# Main parameters
|
|
useTex: bool = False
|
|
isTexRef: bool = False
|
|
isTexCI: bool = False
|
|
texFormat: str = ""
|
|
palFormat: str = ""
|
|
imageDims: tuple[int, int] = (0, 0)
|
|
tmemSize: int = 0
|
|
errorMsg: str = ""
|
|
|
|
# Parameters from moreSetupFromModel
|
|
pal: Optional[list[int]] = None
|
|
palLen: int = 0
|
|
imDependencies: Optional[set[bpy.types.Image]] = None
|
|
flipbook: Optional["TextureFlipbook"] = None
|
|
isPalRef: bool = False
|
|
|
|
# Parameters computed by MultitexManager.writeAll
|
|
texAddr: int = 0
|
|
palAddr: int = 0
|
|
palIndex: int = 0
|
|
palDependencies: set[bpy.types.Image] = field(default_factory=set)
|
|
palBaseName: str = ""
|
|
loadPal: bool = False
|
|
doTexLoad: bool = True
|
|
doTexTile: bool = True
|
|
|
|
# Internal parameters--copies of passed parameters
|
|
texProp: Optional[TextureProperty] = None
|
|
indexInMat: int = -1
|
|
|
|
def fromMat(self, index: int, f3dMat: F3DMaterialProperty) -> bool:
|
|
useDict = all_combiner_uses(f3dMat)
|
|
if not useDict["Texture " + str(index)]:
|
|
return True
|
|
|
|
texProp = getattr(f3dMat, "tex" + str(index))
|
|
return self.fromProp(texProp, index)
|
|
|
|
def fromProp(self, texProp: TextureProperty, index: int, ignore_tex_set=False) -> bool:
|
|
self.indexInMat = index
|
|
self.texProp = texProp
|
|
if not texProp.tex_set and not ignore_tex_set:
|
|
return True
|
|
|
|
self.useTex = True
|
|
tex = texProp.tex
|
|
self.isTexRef = texProp.use_tex_reference
|
|
self.texFormat = texProp.tex_format
|
|
self.isTexCI = self.texFormat[:2] == "CI"
|
|
self.palFormat = texProp.ci_format if self.isTexCI else ""
|
|
|
|
if tex is not None and (tex.size[0] == 0 or tex.size[1] == 0):
|
|
self.errorMsg = f"Image {tex.name} has 0 size; may have been deleted/moved."
|
|
return False
|
|
|
|
if not self.isTexRef:
|
|
if tex is None:
|
|
self.errorMsg = f"No texture is selected."
|
|
return False
|
|
elif len(tex.pixels) == 0:
|
|
self.errorMsg = f"Image {tex.name} is missing on disk."
|
|
return False
|
|
|
|
if self.isTexRef:
|
|
width, height = texProp.tex_reference_size
|
|
else:
|
|
width, height = tex.size
|
|
self.imageDims = (width, height)
|
|
|
|
self.tmemSize = getTmemWordUsage(self.texFormat, width, height)
|
|
|
|
if width > 1024 or height > 1024:
|
|
self.errorMsg = f"Image size (even large textures) limited to 1024 in each dimension."
|
|
return False
|
|
|
|
if texBitSizeInt[self.texFormat] == 4 and (width & 1) != 0:
|
|
self.errorMsg = f"A 4-bit image must have a width which is even."
|
|
return False
|
|
|
|
return True
|
|
|
|
def materialless_setup(self) -> None:
|
|
"""moreSetupFromModel equivalent that does not handle material properties like OOT flipbooks"""
|
|
if not self.useTex:
|
|
return
|
|
|
|
if self.isTexCI:
|
|
self.imDependencies, self.flipbook, self.pal = (
|
|
set() if self.texProp.tex is None else {self.texProp.tex},
|
|
None,
|
|
None,
|
|
)
|
|
if self.isTexRef:
|
|
self.palLen = self.texProp.pal_reference_size
|
|
else:
|
|
assert self.flipbook is None
|
|
self.pal = getColorsUsedInImage(self.texProp.tex, self.palFormat)
|
|
self.palLen = len(self.pal)
|
|
if self.palLen > (16 if self.texFormat == "CI4" else 256):
|
|
raise PluginError(
|
|
f"Error in Texture {self.indexInMat} uses too many unique colors to fit in format {self.texFormat}."
|
|
)
|
|
else:
|
|
self.imDependencies = set() if self.texProp.tex is None else {self.texProp.tex}
|
|
self.flipbook = None
|
|
|
|
self.isPalRef = self.isTexRef and self.flipbook is None
|
|
self.palDependencies = self.imDependencies
|
|
|
|
def moreSetupFromModel(
|
|
self,
|
|
material: bpy.types.Material,
|
|
fMaterial: FMaterial,
|
|
fModel: FModel,
|
|
) -> None:
|
|
if not self.useTex:
|
|
return
|
|
|
|
if self.isTexCI:
|
|
self.imDependencies, self.flipbook, self.pal = fModel.processTexRefCITextures(
|
|
fMaterial, material, self.indexInMat
|
|
)
|
|
if self.isTexRef:
|
|
if self.flipbook is not None:
|
|
self.palLen = len(self.pal)
|
|
else:
|
|
self.palLen = self.texProp.pal_reference_size
|
|
else:
|
|
assert self.flipbook is None
|
|
self.pal = getColorsUsedInImage(self.texProp.tex, self.palFormat)
|
|
self.palLen = len(self.pal)
|
|
if self.palLen > (16 if self.texFormat == "CI4" else 256):
|
|
raise PluginError(
|
|
f"Texture {self.indexInMat}"
|
|
+ (" (all flipbook textures)" if self.flipbook is not None else "")
|
|
+ f" uses too many unique colors to fit in format {self.texFormat}."
|
|
)
|
|
else:
|
|
self.imDependencies, self.flipbook = fModel.processTexRefNonCITextures(fMaterial, material, self.indexInMat)
|
|
|
|
self.isPalRef = self.isTexRef and self.flipbook is None
|
|
self.palDependencies = self.imDependencies
|
|
|
|
def getPaletteName(self):
|
|
if not self.useTex or self.isPalRef:
|
|
return None
|
|
if self.flipbook is not None:
|
|
return self.flipbook.name
|
|
return getImageName(self.texProp.tex)
|
|
|
|
def setup_single_tex(self, is_ci: bool, use_large_tex: bool):
|
|
is_large = False
|
|
tmem_size = 256 if is_ci else 512
|
|
if is_ci:
|
|
assert self.useTex # should this be here?
|
|
if self.useTex:
|
|
self.loadPal = True
|
|
self.palBaseName = self.getPaletteName()
|
|
if self.tmemSize > tmem_size:
|
|
if use_large_tex:
|
|
self.doTexLoad = False
|
|
return True
|
|
elif not bpy.context.scene.ignoreTextureRestrictions:
|
|
raise PluginError(
|
|
"Textures are too big. Max TMEM size is 4k "
|
|
"bytes, ex. 2 32x32 RGBA 16 bit textures.\n"
|
|
"Note that texture width will be internally padded to 64 bit boundaries."
|
|
)
|
|
return is_large
|
|
|
|
def writeAll(
|
|
self,
|
|
fMaterial: FMaterial,
|
|
fModel: Union[FModel, FTexRect],
|
|
convertTextureData: bool,
|
|
):
|
|
if not self.useTex:
|
|
return
|
|
assert (
|
|
self.imDependencies is not None
|
|
), "self.imDependencies is None, either moreSetupFromModel or materialless_setup must be called beforehand"
|
|
|
|
# Get definitions
|
|
imageKey, fImage = saveOrGetTextureDefinition(
|
|
fMaterial, fModel, self.texProp, self.imDependencies, fMaterial.isTexLarge[self.indexInMat]
|
|
)
|
|
fMaterial.imageKey[self.indexInMat] = imageKey
|
|
if self.loadPal:
|
|
_, fPalette = saveOrGetPaletteDefinition(
|
|
fMaterial, fModel, self.texProp, self.isPalRef, self.palDependencies, self.palBaseName, self.palLen
|
|
)
|
|
|
|
# Write loads
|
|
loadGfx = fMaterial.texture_DL
|
|
f3d = fModel.f3d
|
|
if self.loadPal:
|
|
savePaletteLoad(loadGfx, fPalette, self.palFormat, self.palAddr, self.palLen, 5 - self.indexInMat, f3d)
|
|
if self.doTexLoad:
|
|
saveTextureLoadOnly(fImage, loadGfx, self.texProp, None, 7 - self.indexInMat, self.texAddr, f3d)
|
|
if self.doTexTile:
|
|
saveTextureTile(
|
|
fImage, fMaterial, loadGfx, self.texProp, None, self.indexInMat, self.texAddr, self.palIndex, f3d
|
|
)
|
|
|
|
# Write texture data
|
|
if convertTextureData:
|
|
if self.loadPal and not self.isPalRef:
|
|
writePaletteData(fPalette, self.pal)
|
|
if self.isTexRef:
|
|
if self.isTexCI:
|
|
fModel.writeTexRefCITextures(
|
|
self.flipbook, fMaterial, self.imDependencies, self.pal, self.texFormat, self.palFormat
|
|
)
|
|
else:
|
|
fModel.writeTexRefNonCITextures(self.flipbook, self.texFormat)
|
|
else:
|
|
if self.isTexCI:
|
|
assert (
|
|
self.pal is not None
|
|
), "self.pal is None, either moreSetupFromModel or materialless_setup must be called beforehand"
|
|
writeCITextureData(self.texProp.tex, fImage, self.pal, self.palFormat, self.texFormat)
|
|
else:
|
|
writeNonCITextureData(self.texProp.tex, fImage, self.texFormat)
|
|
|
|
|
|
class MultitexManager:
|
|
def __init__(
|
|
self,
|
|
material: bpy.types.Material,
|
|
fMaterial: FMaterial,
|
|
fModel: FModel,
|
|
):
|
|
f3dMat = material.f3d_mat
|
|
self.ti0, self.ti1 = TexInfo(), TexInfo()
|
|
if not self.ti0.fromMat(0, f3dMat):
|
|
raise PluginError(f"Tex0: {self.ti0.errorMsg}")
|
|
if not self.ti1.fromMat(1, f3dMat):
|
|
raise PluginError(f"Tex1: {self.ti1.errorMsg}")
|
|
self.ti0.moreSetupFromModel(material, fMaterial, fModel)
|
|
self.ti1.moreSetupFromModel(material, fMaterial, fModel)
|
|
|
|
self.isCI = self.ti0.isTexCI or self.ti1.isTexCI
|
|
|
|
if self.ti0.useTex and self.ti1.useTex:
|
|
if self.ti0.isTexCI != self.ti1.isTexCI:
|
|
raise PluginError("N64 does not support CI + non-CI texture. Must be both CI or neither CI.")
|
|
if (
|
|
self.ti0.isTexRef
|
|
and self.ti1.isTexRef
|
|
and self.ti0.texProp.tex_reference == self.ti1.texProp.tex_reference
|
|
and self.ti0.texProp.tex_reference_size != self.ti1.texProp.tex_reference_size
|
|
):
|
|
raise PluginError("Two textures with the same reference must have the same size.")
|
|
if self.isCI:
|
|
if self.ti0.palFormat != self.ti1.palFormat:
|
|
raise PluginError("Both CI textures must use the same palette format (usually RGBA16).")
|
|
if (
|
|
self.ti0.isTexRef
|
|
and self.ti1.isTexRef
|
|
and self.ti0.texProp.pal_reference == self.ti1.texProp.pal_reference
|
|
and self.ti0.texProp.pal_reference_size != self.ti1.texProp.pal_reference_size
|
|
):
|
|
raise PluginError("Two textures with the same palette reference must have the same palette size.")
|
|
|
|
self.palFormat = self.ti0.palFormat if self.ti0.useTex else self.ti1.palFormat
|
|
|
|
def writeAll(
|
|
self, material: bpy.types.Material, fMaterial: FMaterial, fModel: FModel, convertTextureData: bool
|
|
) -> None:
|
|
f3dMat = material.f3d_mat
|
|
# Determine how to arrange / load palette entries into upper half of tmem
|
|
if self.isCI:
|
|
assert self.ti0.useTex or self.ti1.useTex
|
|
if not self.ti1.useTex:
|
|
self.ti0.loadPal = True
|
|
elif not self.ti0.useTex:
|
|
self.ti1.loadPal = True
|
|
elif not convertTextureData:
|
|
if self.ti0.texFormat == "CI8" or self.ti1.texFormat == "CI8":
|
|
raise PluginError(
|
|
"When using export as PNGs mode, can't have multitexture with one or more CI8 textures."
|
|
+ " Only single CI texture or two CI4 textures."
|
|
)
|
|
self.ti0.loadPal = self.ti1.loadPal = True
|
|
self.ti1.palIndex = 1
|
|
self.ti1.palAddr = 16
|
|
else: # Two CI textures, normal mode
|
|
if self.ti0.texFormat == "CI8" and self.ti1.texFormat == "CI8":
|
|
if (self.ti0.pal is None) != (self.ti1.pal is None):
|
|
raise PluginError(
|
|
"Can't have two CI8 textures where only one is a non-flipbook reference; "
|
|
+ "no way to assign the palette."
|
|
)
|
|
self.ti0.loadPal = True
|
|
if self.ti0.pal is None:
|
|
if self.ti0.texProp.pal_reference != self.ti1.texProp.pal_reference:
|
|
raise PluginError("Can't have two CI8 textures with different palette references.")
|
|
else:
|
|
self.ti0.pal = mergePalettes(self.ti0.pal, self.ti1.pal)
|
|
self.ti0.palLen = len(self.ti0.pal)
|
|
if self.ti0.palLen > 256:
|
|
raise PluginError(
|
|
"The two CI textures together contain a total of "
|
|
+ str(self.ti0.palLen)
|
|
+ " colors, which can't fit in a CI8 palette (256)."
|
|
)
|
|
# self.ti0.imDependencies remains what it was; the CIs in im0 are the same as they
|
|
# would be if im0 was alone. But im1 and self.ti0.pal depend on both.
|
|
self.ti1.imDependencies = self.ti0.palDependencies = (
|
|
self.ti0.imDependencies | self.ti1.imDependencies
|
|
)
|
|
elif self.ti0.texFormat != self.ti1.texFormat: # One CI8, one CI4
|
|
ci8Pal, ci4Pal = (
|
|
(self.ti0.pal, self.ti1.pal) if self.ti0.texFormat == "CI8" else (self.ti1.pal, self.ti0.pal)
|
|
)
|
|
ci8PalLen, ci4PalLen = (
|
|
(self.ti0.palLen, self.ti1.palLen)
|
|
if self.ti0.texFormat == "CI8"
|
|
else (self.ti1.palLen, self.ti0.palLen)
|
|
)
|
|
if self.ti0.pal is None or self.ti1.pal is None:
|
|
if ci8PalLen > 256 - 16:
|
|
raise PluginError(
|
|
"The CI8 texture has over 240 colors, which can't fit together with the CI4 palette."
|
|
)
|
|
self.ti0.loadPal = self.ti1.loadPal = True
|
|
if self.ti0.texFormat == "CI8":
|
|
self.ti1.palIndex = 15
|
|
self.ti1.palAddr = 240
|
|
else:
|
|
self.ti0.palIndex = 15
|
|
self.ti0.palAddr = 240
|
|
else:
|
|
# CI4 indices in palette 0, CI8 indices start from palette 0
|
|
self.ti0.loadPal = True
|
|
self.ti0.pal = mergePalettes(ci4Pal, ci8Pal)
|
|
self.ti0.palLen = len(self.ti0.pal)
|
|
if self.ti0.palLen > 256:
|
|
raise PluginError(
|
|
"The two CI textures together contain a total of "
|
|
+ str(self.ti0.palLen)
|
|
+ " colors, which can't fit in a CI8 palette (256)."
|
|
+ " The CI8 texture must contain up to 240 unique colors,"
|
|
+ " plus the same up to 16 colors used in the CI4 texture."
|
|
)
|
|
# The use for the CI4 texture remains what it was; its CIs are the
|
|
# same as if it was alone. But both the palette and the CI8 CIs are affected.
|
|
self.ti0.palDependencies = self.ti0.imDependencies | self.ti1.imDependencies
|
|
if self.ti0.texFormat == "CI8":
|
|
self.ti0.imDependencies = self.ti0.palDependencies
|
|
else:
|
|
self.ti1.imDependencies = self.ti0.palDependencies
|
|
else: # both CI4 textures
|
|
if (
|
|
self.ti0.pal is None
|
|
and self.ti1.pal is None
|
|
and self.ti0.texProp.pal_reference == self.ti1.texProp.pal_reference
|
|
):
|
|
self.ti0.loadPal = True
|
|
elif self.ti0.pal is None or self.ti1.pal is None:
|
|
self.ti0.loadPal = self.ti1.loadPal = True
|
|
self.ti1.palIndex = 1
|
|
self.ti1.palAddr = 16
|
|
else:
|
|
self.ti0.loadPal = True
|
|
tempPal = mergePalettes(self.ti0.pal, self.ti1.pal)
|
|
tempPalLen = len(tempPal)
|
|
assert tempPalLen <= 32
|
|
if tempPalLen <= 16:
|
|
# Share palette 0
|
|
self.ti0.pal = tempPal
|
|
self.ti0.palLen = tempPalLen
|
|
# self.ti0.imDependencies remains what it was; the CIs in im0 are the same as they
|
|
# would be if im0 was alone. But im1 and self.ti0.pal depend on both.
|
|
self.ti1.imDependencies = self.ti0.palDependencies = (
|
|
self.ti0.imDependencies | self.ti1.imDependencies
|
|
)
|
|
else:
|
|
# Load one palette across 0-1. Put the longer in slot 0
|
|
if self.ti0.palLen >= self.ti1.palLen:
|
|
while len(self.ti0.pal) < 16:
|
|
self.ti0.pal.append(0)
|
|
self.ti0.pal.extend(self.ti1.pal)
|
|
self.ti0.palLen = len(self.ti0.pal)
|
|
self.ti1.palIndex = 1
|
|
else:
|
|
while len(self.ti1.pal) < 16:
|
|
self.ti1.pal.append(0)
|
|
self.ti0.pal = self.ti1.pal + self.ti0.pal
|
|
self.ti0.palLen = len(self.ti0.pal)
|
|
self.ti0.palIndex = 1
|
|
# The up-to-32 entries in self.ti0.pal depend on both images. But the
|
|
# CIs in both im0 and im1 are the same as if there was no shared palette.
|
|
self.ti0.palDependencies = self.ti0.imDependencies | self.ti1.imDependencies
|
|
fMaterial.texPaletteIndex = [self.ti0.palIndex, self.ti1.palIndex]
|
|
self.ti0.palBaseName = self.ti0.getPaletteName()
|
|
self.ti1.palBaseName = self.ti1.getPaletteName()
|
|
if self.isCI and self.ti0.useTex and self.ti1.useTex and not self.ti1.loadPal:
|
|
self.ti0.palBaseName = self.ti0.palBaseName + "_x_" + self.ti1.palBaseName
|
|
self.ti1.pal = self.ti0.pal
|
|
|
|
# Assign TMEM addresses
|
|
sameTextures = (
|
|
(self.ti0.useTex and self.ti1.useTex)
|
|
and self.ti0.isTexRef == self.ti1.isTexRef
|
|
and self.ti0.tmemSize == self.ti1.tmemSize
|
|
and self.ti0.texFormat == self.ti1.texFormat
|
|
and (
|
|
( # not a reference
|
|
not self.ti0.isTexRef and self.ti0.texProp.tex == self.ti1.texProp.tex # same image
|
|
)
|
|
or ( # reference
|
|
self.ti0.isTexRef
|
|
and self.ti0.texProp.tex_reference == self.ti1.texProp.tex_reference
|
|
and self.ti0.texProp.tex_reference_size == self.ti1.texProp.tex_reference_size
|
|
and ( # ci format reference
|
|
not self.isCI
|
|
or (
|
|
self.ti0.texProp.pal_reference == self.ti1.texProp.pal_reference
|
|
and self.ti0.texProp.pal_reference_size == self.ti1.texProp.pal_reference_size
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
useLargeTextures = material.mat_ver > 3 and f3dMat.use_large_textures
|
|
tmemSize = 256 if self.isCI else 512
|
|
self.ti1.texAddr = None # must be set whenever tex 1 used (and loaded or tiled)
|
|
tmemOccupied = self.texDimensions = None # must be set on all codepaths
|
|
if sameTextures:
|
|
assert (
|
|
self.ti0.tmemSize == self.ti1.tmemSize
|
|
), f"Unreachable code path, same textures (same image or reference) somehow not the same size"
|
|
tmemOccupied = self.ti0.tmemSize
|
|
self.ti1.doTexLoad = False
|
|
self.ti1.texAddr = 0
|
|
self.texDimensions = self.ti0.imageDims
|
|
fMaterial.largeTexFmt = self.ti0.texFormat
|
|
elif not useLargeTextures or self.ti0.tmemSize + self.ti1.tmemSize <= tmemSize:
|
|
self.ti1.texAddr = self.ti0.tmemSize
|
|
tmemOccupied = self.ti0.tmemSize + self.ti1.tmemSize
|
|
if not self.ti0.useTex and not self.ti1.useTex:
|
|
self.texDimensions = [32, 32]
|
|
fMaterial.largeTexFmt = "RGBA16"
|
|
elif not self.ti1.useTex or f3dMat.uv_basis == "TEXEL0":
|
|
self.texDimensions = self.ti0.imageDims
|
|
fMaterial.largeTexFmt = self.ti0.texFormat
|
|
else:
|
|
self.texDimensions = self.ti1.imageDims
|
|
fMaterial.largeTexFmt = self.ti1.texFormat
|
|
else: # useLargeTextures
|
|
if self.ti0.useTex and self.ti1.useTex:
|
|
tmemOccupied = tmemSize
|
|
# TODO: Could change this in the future to do the face tile assigments
|
|
# first, to see how large a tile the large texture(s) needed, instead
|
|
# of arbitrarily assigning half of TMEM to each of the two textures.
|
|
if self.ti0.tmemSize <= tmemSize // 2:
|
|
# Tex 0 normal, tex 1 large
|
|
self.texDimensions = self.ti1.imageDims
|
|
fMaterial.largeTexFmt = self.ti1.texFormat
|
|
fMaterial.isTexLarge[1] = True
|
|
fMaterial.largeTexAddr[1] = self.ti0.tmemSize
|
|
fMaterial.largeTexWords = tmemSize - self.ti0.tmemSize
|
|
self.ti1.doTexLoad = self.ti1.doTexTile = False
|
|
elif self.ti1.tmemSize <= tmemSize // 2:
|
|
# Tex 0 large, tex 1 normal
|
|
self.texDimensions = self.ti0.imageDims
|
|
fMaterial.largeTexFmt = self.ti0.texFormat
|
|
fMaterial.isTexLarge[0] = True
|
|
fMaterial.largeTexAddr[0] = 0
|
|
fMaterial.largeTexWords = tmemSize - self.ti1.tmemSize
|
|
self.ti0.doTexLoad = self.ti0.doTexTile = False
|
|
self.ti1.texAddr = tmemSize - self.ti1.tmemSize
|
|
else:
|
|
# Both textures large
|
|
raise PluginError("Multitexture with two large textures is not currently supported.")
|
|
# Limited cases of 2x large textures could be supported in the
|
|
# future. However, these cases are either of questionable
|
|
# utility or have substantial restrictions. Most cases could be
|
|
# premixed into one texture, or would run out of UV space for
|
|
# tiling (1024x1024 in the space of whichever texture had
|
|
# smaller pixels), or one of the textures could be non-large.
|
|
if f3dMat.uv_basis == "TEXEL0":
|
|
self.texDimensions = self.ti0.imageDims
|
|
fMaterial.largeTexFmt = self.ti0.texFormat
|
|
else:
|
|
self.texDimensions = self.ti1.imageDims
|
|
fMaterial.largeTexFmt = self.ti1.texFormat
|
|
fMaterial.isTexLarge[0] = True
|
|
fMaterial.isTexLarge[1] = True
|
|
fMaterial.largeTexAddr[0] = 0
|
|
fMaterial.largeTexAddr[1] = tmemSize // 2
|
|
fMaterial.largeTexWords = tmemSize // 2
|
|
self.ti0.doTexLoad = self.ti0.doTexTile = self.ti1.doTexLoad = self.ti1.doTexTile = False
|
|
elif self.ti0.useTex:
|
|
self.texDimensions = self.ti0.imageDims
|
|
fMaterial.largeTexFmt = self.ti0.texFormat
|
|
fMaterial.isTexLarge[0] = True
|
|
fMaterial.largeTexAddr[0] = 0
|
|
fMaterial.largeTexWords = tmemSize
|
|
self.ti0.doTexLoad = self.ti0.doTexTile = False
|
|
tmemOccupied = tmemSize
|
|
elif self.ti1.useTex:
|
|
self.ti1.texAddr = 0
|
|
self.texDimensions = self.ti1.imageDims
|
|
fMaterial.largeTexFmt = self.ti1.texFormat
|
|
fMaterial.isTexLarge[1] = True
|
|
fMaterial.largeTexAddr[1] = 0
|
|
fMaterial.largeTexWords = tmemSize
|
|
self.ti1.doTexLoad = self.ti1.doTexTile = False
|
|
tmemOccupied = tmemSize
|
|
if tmemOccupied > tmemSize:
|
|
if sameTextures and useLargeTextures:
|
|
raise PluginError("Using the same texture for Tex0 and Tex1 is not compatible with large textures.")
|
|
elif not bpy.context.scene.ignoreTextureRestrictions:
|
|
raise PluginError(
|
|
"Textures are too big. Max TMEM size is 4k "
|
|
+ "bytes, ex. 2 32x32 RGBA 16 bit textures.\nNote that texture width will be internally padded to 64 bit boundaries."
|
|
)
|
|
|
|
self.ti0.writeAll(fMaterial, fModel, convertTextureData)
|
|
self.ti1.writeAll(fMaterial, fModel, convertTextureData)
|
|
|
|
def getTexDimensions(self):
|
|
return self.texDimensions
|
|
|
|
|
|
# Functions for writing texture and palette DLs
|
|
|
|
|
|
def getTileSizeSettings(texProp: TextureProperty, tileSettings: Optional[TileLoad], f3d: F3D):
|
|
if tileSettings is not None:
|
|
SL = tileSettings.sl
|
|
TL = tileSettings.tl
|
|
SH = tileSettings.sh
|
|
TH = tileSettings.th
|
|
else:
|
|
SL = texProp.S.low
|
|
TL = texProp.T.low
|
|
SH = texProp.S.high
|
|
TH = texProp.T.high
|
|
sl = int(SL * (2**f3d.G_TEXTURE_IMAGE_FRAC))
|
|
tl = int(TL * (2**f3d.G_TEXTURE_IMAGE_FRAC))
|
|
sh = int(SH * (2**f3d.G_TEXTURE_IMAGE_FRAC))
|
|
th = int(TH * (2**f3d.G_TEXTURE_IMAGE_FRAC))
|
|
return SL, TL, SH, TH, sl, tl, sh, th
|
|
|
|
|
|
def getTileLine(fImage: FImage, SL: int, SH: int, siz: str, f3d: F3D):
|
|
width = int(SH - SL + 1) if fImage.isLargeTexture else int(fImage.width)
|
|
if siz == "G_IM_SIZ_4b":
|
|
line = (((width + 1) >> 1) + 7) >> 3
|
|
else:
|
|
# Note that _LINE_BYTES and _TILE_BYTES variables are the same.
|
|
line = int((width * f3d.G_IM_SIZ_VARS[siz + "_LINE_BYTES"]) + 7) >> 3
|
|
return line
|
|
|
|
|
|
def canUseLoadBlock(fImage: FImage, tex_format: str, f3d: F3D):
|
|
if fImage.isLargeTexture:
|
|
return False
|
|
width, height = fImage.width, fImage.height
|
|
texelsPerWord = 64 // texBitSizeInt[tex_format]
|
|
if width % texelsPerWord != 0:
|
|
return False
|
|
wordsperrow = width // texelsPerWord
|
|
dxt = ((1 << f3d.G_TX_DXT_FRAC) + wordsperrow - 1) // wordsperrow
|
|
error = (dxt * wordsperrow) - (1 << f3d.G_TX_DXT_FRAC)
|
|
assert error >= 0
|
|
if error == 0:
|
|
return True
|
|
rowsWhenCorruptionHappens = (dxt + error - 1) // error
|
|
return height < rowsWhenCorruptionHappens
|
|
|
|
|
|
def saveTextureLoadOnly(
|
|
fImage: FImage,
|
|
gfxOut: GfxList,
|
|
texProp: TextureProperty,
|
|
tileSettings: Optional[TileLoad],
|
|
loadtile: int,
|
|
tmem: int,
|
|
f3d: F3D,
|
|
omitSetTextureImage=False,
|
|
omitSetTile=False,
|
|
):
|
|
fmt = texFormatOf[texProp.tex_format]
|
|
siz = texBitSizeF3D[texProp.tex_format]
|
|
nocm = ("G_TX_WRAP", "G_TX_NOMIRROR")
|
|
SL, TL, SH, TH, sl, tl, sh, th = getTileSizeSettings(texProp, tileSettings, f3d)
|
|
|
|
# LoadTile will pad rows to 64 bit word alignment, while
|
|
# LoadBlock assumes this is already done.
|
|
useLoadBlock = canUseLoadBlock(fImage, texProp.tex_format, f3d)
|
|
line = 0 if useLoadBlock else getTileLine(fImage, SL, SH, siz, f3d)
|
|
wid = 1 if useLoadBlock else fImage.width
|
|
|
|
if siz == "G_IM_SIZ_4b":
|
|
if useLoadBlock:
|
|
dxs = (((fImage.width) * (fImage.height) + 3) >> 2) - 1
|
|
dxt = f3d.CALC_DXT_4b(fImage.width)
|
|
siz = "G_IM_SIZ_16b"
|
|
loadCommand = DPLoadBlock(loadtile, 0, 0, dxs, dxt)
|
|
else:
|
|
sl2 = int(SL * (2 ** (f3d.G_TEXTURE_IMAGE_FRAC - 1)))
|
|
sh2 = int(SH * (2 ** (f3d.G_TEXTURE_IMAGE_FRAC - 1)))
|
|
siz = "G_IM_SIZ_8b"
|
|
wid >>= 1
|
|
loadCommand = DPLoadTile(loadtile, sl2, tl, sh2, th)
|
|
else:
|
|
if useLoadBlock:
|
|
dxs = (
|
|
((fImage.width) * (fImage.height) + f3d.G_IM_SIZ_VARS[siz + "_INCR"])
|
|
>> f3d.G_IM_SIZ_VARS[siz + "_SHIFT"]
|
|
) - 1
|
|
dxt = f3d.CALC_DXT(fImage.width, f3d.G_IM_SIZ_VARS[siz + "_BYTES"])
|
|
siz += "_LOAD_BLOCK"
|
|
loadCommand = DPLoadBlock(loadtile, 0, 0, dxs, dxt)
|
|
else:
|
|
loadCommand = DPLoadTile(loadtile, sl, tl, sh, th)
|
|
|
|
if not omitSetTextureImage:
|
|
gfxOut.commands.append(DPSetTextureImage(fmt, siz, wid, fImage))
|
|
if not omitSetTile:
|
|
gfxOut.commands.append(DPSetTile(fmt, siz, line, tmem, loadtile, 0, nocm, 0, 0, nocm, 0, 0))
|
|
gfxOut.commands.append(loadCommand)
|
|
|
|
|
|
def saveTextureTile(
|
|
fImage: FImage,
|
|
fMaterial: FMaterial,
|
|
gfxOut: GfxList,
|
|
texProp: TextureProperty,
|
|
tileSettings,
|
|
rendertile: int,
|
|
tmem: int,
|
|
pal: int,
|
|
f3d: F3D,
|
|
omitSetTile=False,
|
|
):
|
|
if tileSettings is not None:
|
|
clamp_S = True
|
|
clamp_T = True
|
|
mirror_S = False
|
|
mirror_T = False
|
|
mask_S = 0
|
|
mask_T = 0
|
|
shift_S = 0
|
|
shift_T = 0
|
|
else:
|
|
clamp_S = texProp.S.clamp
|
|
clamp_T = texProp.T.clamp
|
|
mirror_S = texProp.S.mirror
|
|
mirror_T = texProp.T.mirror
|
|
mask_S = texProp.S.mask
|
|
mask_T = texProp.T.mask
|
|
shift_S = texProp.S.shift
|
|
shift_T = texProp.T.shift
|
|
cms = (("G_TX_CLAMP" if clamp_S else "G_TX_WRAP"), ("G_TX_MIRROR" if mirror_S else "G_TX_NOMIRROR"))
|
|
cmt = (("G_TX_CLAMP" if clamp_T else "G_TX_WRAP"), ("G_TX_MIRROR" if mirror_T else "G_TX_NOMIRROR"))
|
|
masks = mask_S
|
|
maskt = mask_T
|
|
shifts = shift_S if shift_S >= 0 else (shift_S + 16)
|
|
shiftt = shift_T if shift_T >= 0 else (shift_T + 16)
|
|
fmt = texFormatOf[texProp.tex_format]
|
|
siz = texBitSizeF3D[texProp.tex_format]
|
|
SL, _, SH, _, sl, tl, sh, th = getTileSizeSettings(texProp, tileSettings, f3d)
|
|
line = getTileLine(fImage, SL, SH, siz, f3d)
|
|
|
|
tileCommand = DPSetTile(fmt, siz, line, tmem, rendertile, pal, cmt, maskt, shiftt, cms, masks, shifts)
|
|
tileSizeCommand = DPSetTileSize(rendertile, sl, tl, sh, th)
|
|
|
|
scrollInfo = getattr(fMaterial.scrollData, f"tile_scroll_tex{rendertile}")
|
|
if scrollInfo.s or scrollInfo.t:
|
|
tileSizeCommand.tags |= GfxTag.TileScroll0 if rendertile == 0 else GfxTag.TileScroll1
|
|
|
|
tileSizeCommand.fMaterial = fMaterial
|
|
if not omitSetTile:
|
|
gfxOut.commands.append(tileCommand)
|
|
gfxOut.commands.append(tileSizeCommand)
|
|
|
|
# hasattr check for FTexRect
|
|
if hasattr(fMaterial, "tileSizeCommands"):
|
|
fMaterial.tileSizeCommands[rendertile] = tileSizeCommand
|
|
|
|
|
|
# palAddr is the address within the second half of tmem (0-255), normally 16*palette num
|
|
# palLen is the number of colors
|
|
def savePaletteLoad(
|
|
gfxOut: GfxList,
|
|
fPalette: FImage,
|
|
palFormat: str,
|
|
palAddr: int,
|
|
palLen: int,
|
|
loadtile: int,
|
|
f3d: F3D,
|
|
):
|
|
assert 0 <= palAddr < 256 and (palAddr & 0xF) == 0
|
|
palFmt = texFormatOf[palFormat]
|
|
nocm = ("G_TX_WRAP", "G_TX_NOMIRROR")
|
|
gfxOut.commands.extend(
|
|
[
|
|
DPSetTextureImage(palFmt, "G_IM_SIZ_16b", 1, fPalette),
|
|
DPSetTile("0", "0", 0, 256 + palAddr, loadtile, 0, nocm, 0, 0, nocm, 0, 0),
|
|
DPLoadTLUTCmd(loadtile, palLen - 1),
|
|
]
|
|
)
|
|
|
|
|
|
# Functions for converting and writing texture and palette data
|
|
|
|
|
|
def extractConvertCIPixel(image, pixels, i, j, palFormat):
|
|
color = [1, 1, 1, 1]
|
|
for field in range(image.channels):
|
|
color[field] = pixels[(j * image.size[0] + i) * image.channels + field]
|
|
if palFormat == "RGBA16":
|
|
pixelColor = getRGBA16Tuple(color)
|
|
elif palFormat == "IA16":
|
|
pixelColor = getIA16Tuple(color)
|
|
else:
|
|
raise PluginError("Internal error, palette format is " + palFormat)
|
|
return pixelColor
|
|
|
|
|
|
def getColorsUsedInImage(image, palFormat):
|
|
palette = []
|
|
# N64 is -Y, Blender is +Y
|
|
pixels = image.pixels[:]
|
|
for j in reversed(range(image.size[1])):
|
|
for i in range(image.size[0]):
|
|
pixelColor = extractConvertCIPixel(image, pixels, i, j, palFormat)
|
|
if pixelColor not in palette:
|
|
palette.append(pixelColor)
|
|
return palette
|
|
|
|
|
|
def mergePalettes(pal0, pal1):
|
|
palette = [c for c in pal0]
|
|
for c in pal1:
|
|
if c not in palette:
|
|
palette.append(c)
|
|
return palette
|
|
|
|
|
|
def getColorIndicesOfTexture(image, palette, palFormat):
|
|
texture = []
|
|
# N64 is -Y, Blender is +Y
|
|
pixels = image.pixels[:]
|
|
for j in reversed(range(image.size[1])):
|
|
for i in range(image.size[0]):
|
|
pixelColor = extractConvertCIPixel(image, pixels, i, j, palFormat)
|
|
if pixelColor not in palette:
|
|
raise PluginError(f"Bug: {image.name} palette len {len(palette)} missing CI")
|
|
texture.append(palette.index(pixelColor))
|
|
return texture
|
|
|
|
|
|
def compactNibbleArray(texture, width, height):
|
|
nibbleData = bytearray(0)
|
|
dataSize = int(width * height / 2)
|
|
|
|
nibbleData = [((texture[i * 2] & 0xF) << 4) | (texture[i * 2 + 1] & 0xF) for i in range(dataSize)]
|
|
|
|
if (width * height) % 2 == 1:
|
|
nibbleData.append((texture[-1] & 0xF) << 4)
|
|
|
|
return bytearray(nibbleData)
|
|
|
|
|
|
def writePaletteData(fPalette: FImage, palette: list[int]):
|
|
if fPalette.converted:
|
|
return
|
|
for color in palette:
|
|
fPalette.data.extend(color.to_bytes(2, "big"))
|
|
fPalette.converted = True
|
|
|
|
|
|
def writeCITextureData(
|
|
image: bpy.types.Image,
|
|
fImage: FImage,
|
|
palette: list[int],
|
|
palFmt: str,
|
|
texFmt: str,
|
|
):
|
|
if fImage.converted:
|
|
return
|
|
|
|
texture = getColorIndicesOfTexture(image, palette, palFmt)
|
|
|
|
if texFmt == "CI4":
|
|
fImage.data = compactNibbleArray(texture, image.size[0], image.size[1])
|
|
else:
|
|
fImage.data = bytearray(texture)
|
|
fImage.converted = True
|
|
|
|
|
|
def writeNonCITextureData(image: bpy.types.Image, fImage: FImage, texFmt: str):
|
|
if fImage.converted:
|
|
return
|
|
fmt = texFormatOf[texFmt]
|
|
bitSize = texBitSizeF3D[texFmt]
|
|
|
|
pixels = image.pixels[:]
|
|
if fmt == "G_IM_FMT_RGBA":
|
|
if bitSize == "G_IM_SIZ_16b":
|
|
fImage.data = bytearray(
|
|
[
|
|
byteVal
|
|
for doubleByte in [
|
|
(
|
|
(
|
|
((int(round(pixels[(j * image.size[0] + i) * image.channels + 0] * 0x1F)) & 0x1F) << 3)
|
|
| (
|
|
(int(round(pixels[(j * image.size[0] + i) * image.channels + 1] * 0x1F)) & 0x1F)
|
|
>> 2
|
|
)
|
|
),
|
|
(
|
|
((int(round(pixels[(j * image.size[0] + i) * image.channels + 1] * 0x1F)) & 0x03) << 6)
|
|
| (
|
|
(int(round(pixels[(j * image.size[0] + i) * image.channels + 2] * 0x1F)) & 0x1F)
|
|
<< 1
|
|
)
|
|
| (1 if pixels[(j * image.size[0] + i) * image.channels + 3] > 0.5 else 0)
|
|
),
|
|
)
|
|
for j in reversed(range(image.size[1]))
|
|
for i in range(image.size[0])
|
|
]
|
|
for byteVal in doubleByte
|
|
]
|
|
)
|
|
elif bitSize == "G_IM_SIZ_32b":
|
|
fImage.data = bytearray(
|
|
[
|
|
int(round(pixels[(j * image.size[0] + i) * image.channels + field] * 0xFF)) & 0xFF
|
|
for j in reversed(range(image.size[1]))
|
|
for i in range(image.size[0])
|
|
for field in range(image.channels)
|
|
]
|
|
)
|
|
else:
|
|
raise PluginError("Invalid combo: " + fmt + ", " + bitSize)
|
|
|
|
elif fmt == "G_IM_FMT_YUV":
|
|
raise PluginError("YUV not yet implemented.")
|
|
if bitSize == "G_IM_SIZ_16b":
|
|
pass
|
|
else:
|
|
raise PluginError("Invalid combo: " + fmt + ", " + bitSize)
|
|
|
|
elif fmt == "G_IM_FMT_CI":
|
|
raise PluginError("Internal error, writeNonCITextureData called for CI image.")
|
|
|
|
elif fmt == "G_IM_FMT_IA":
|
|
if bitSize == "G_IM_SIZ_4b":
|
|
fImage.data = bytearray(
|
|
[
|
|
(
|
|
(
|
|
int(
|
|
round(
|
|
colorToLuminance(
|
|
pixels[
|
|
(j * image.size[0] + i)
|
|
* image.channels : (j * image.size[0] + i)
|
|
* image.channels
|
|
+ 3
|
|
]
|
|
)
|
|
* 0x7
|
|
)
|
|
)
|
|
& 0x7
|
|
)
|
|
<< 1
|
|
)
|
|
| (1 if pixels[(j * image.size[0] + i) * image.channels + 3] > 0.5 else 0)
|
|
for j in reversed(range(image.size[1]))
|
|
for i in range(image.size[0])
|
|
]
|
|
)
|
|
elif bitSize == "G_IM_SIZ_8b":
|
|
fImage.data = bytearray(
|
|
[
|
|
(
|
|
(
|
|
int(
|
|
round(
|
|
colorToLuminance(
|
|
pixels[
|
|
(j * image.size[0] + i)
|
|
* image.channels : (j * image.size[0] + i)
|
|
* image.channels
|
|
+ 3
|
|
]
|
|
)
|
|
* 0xF
|
|
)
|
|
)
|
|
& 0xF
|
|
)
|
|
<< 4
|
|
)
|
|
| (int(round(pixels[(j * image.size[0] + i) * image.channels + 3] * 0xF)) & 0xF)
|
|
for j in reversed(range(image.size[1]))
|
|
for i in range(image.size[0])
|
|
]
|
|
)
|
|
elif bitSize == "G_IM_SIZ_16b":
|
|
fImage.data = bytearray(
|
|
[
|
|
byteVal
|
|
for doubleByte in [
|
|
(
|
|
int(
|
|
round(
|
|
colorToLuminance(
|
|
pixels[
|
|
(j * image.size[0] + i)
|
|
* image.channels : (j * image.size[0] + i)
|
|
* image.channels
|
|
+ 3
|
|
]
|
|
)
|
|
* 0xFF
|
|
)
|
|
)
|
|
& 0xFF,
|
|
int(round(pixels[(j * image.size[0] + i) * image.channels + 3] * 0xFF)) & 0xFF,
|
|
)
|
|
for j in reversed(range(image.size[1]))
|
|
for i in range(image.size[0])
|
|
]
|
|
for byteVal in doubleByte
|
|
]
|
|
)
|
|
else:
|
|
raise PluginError("Invalid combo: " + fmt + ", " + bitSize)
|
|
elif fmt == "G_IM_FMT_I":
|
|
if bitSize == "G_IM_SIZ_4b":
|
|
fImage.data = bytearray(
|
|
[
|
|
int(
|
|
round(
|
|
colorToLuminance(
|
|
pixels[
|
|
(j * image.size[0] + i) * image.channels : (j * image.size[0] + i) * image.channels
|
|
+ 3
|
|
]
|
|
)
|
|
* 0xF
|
|
)
|
|
)
|
|
& 0xF
|
|
for j in reversed(range(image.size[1]))
|
|
for i in range(image.size[0])
|
|
]
|
|
)
|
|
elif bitSize == "G_IM_SIZ_8b":
|
|
fImage.data = bytearray(
|
|
[
|
|
int(
|
|
round(
|
|
colorToLuminance(
|
|
pixels[
|
|
(j * image.size[0] + i) * image.channels : (j * image.size[0] + i) * image.channels
|
|
+ 3
|
|
]
|
|
)
|
|
* 0xFF
|
|
)
|
|
)
|
|
& 0xFF
|
|
for j in reversed(range(image.size[1]))
|
|
for i in range(image.size[0])
|
|
]
|
|
)
|
|
else:
|
|
raise PluginError("Invalid combo: " + fmt + ", " + bitSize)
|
|
else:
|
|
raise PluginError("Invalid image format " + fmt)
|
|
|
|
# We stored 4bit values in byte arrays, now to convert
|
|
if bitSize == "G_IM_SIZ_4b":
|
|
fImage.data = compactNibbleArray(fImage.data, image.size[0], image.size[1])
|
|
|
|
fImage.converted = True
|