mirror of
https://github.com/modrinth/code.git
synced 2026-09-05 06:19:11 +00:00
feat: add missing ears mod features (#6788)
This commit is contained in:
@@ -210,7 +210,7 @@ function getModelUrlForVariant(variant: string): string {
|
|||||||
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
export const skinBlobUrlMap = reactive(new Map<string, RenderResult>())
|
||||||
export const headBlobUrlMap = reactive(new Map<string, string>())
|
export const headBlobUrlMap = reactive(new Map<string, string>())
|
||||||
const DEBUG_MODE = false
|
const DEBUG_MODE = false
|
||||||
const SKIN_PREVIEW_RENDER_VERSION = 'ears-1'
|
const SKIN_PREVIEW_RENDER_VERSION = 'ears-2'
|
||||||
|
|
||||||
let sharedRenderer: BatchSkinRenderer | null = null
|
let sharedRenderer: BatchSkinRenderer | null = null
|
||||||
let latestPreviewGeneration = 0
|
let latestPreviewGeneration = 0
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* Portions of this file are adapted from Ears and the Ears Skin Manipulator:
|
* Portions of this file are adapted from Ears, Ears2, and the Ears Skin Manipulator:
|
||||||
* https://github.com/exaskye/Ears
|
* https://git.sleeping.town/exa.mods/Ears
|
||||||
* Source revision: 5d050d461d207c1a7cda0de11e2562f6581f7a16
|
* https://git.sleeping.town/exa.mods/Ears2
|
||||||
|
* Source revisions:
|
||||||
|
* - Ears: 5d050d461d207c1a7cda0de11e2562f6581f7a16
|
||||||
|
* - Ears2: 8ad9adc213e3b16b1a3306fb4141cfb618c25a6f
|
||||||
*
|
*
|
||||||
* MIT License
|
* MIT License
|
||||||
*
|
*
|
||||||
@@ -59,16 +62,26 @@ type WingMode =
|
|||||||
| 'ASYMMETRIC_R'
|
| 'ASYMMETRIC_R'
|
||||||
| 'ASYMMETRIC_DUAL'
|
| 'ASYMMETRIC_DUAL'
|
||||||
| 'FLAT'
|
| 'FLAT'
|
||||||
|
type WingAnimationMode = 'NORMAL' | 'NONE' | 'NO_FLIGHT'
|
||||||
|
type LegMode = 'PLANTIGRADE' | 'DIGITIGRADE_PARTIAL' | 'DIGITIGRADE_FULL'
|
||||||
|
type Protrusions =
|
||||||
|
| 'NONE'
|
||||||
|
| 'HORN'
|
||||||
|
| 'CLAWS'
|
||||||
|
| 'CLAWS_AND_HORN'
|
||||||
|
| 'HALO'
|
||||||
|
| 'DOUBLE_HALO'
|
||||||
|
| 'CLAWS_AND_HALO'
|
||||||
|
| 'CLAWS_AND_DOUBLE_HALO'
|
||||||
type BodyPart = 'HEAD' | 'TORSO' | 'LEFT_ARM' | 'RIGHT_ARM' | 'LEFT_LEG' | 'RIGHT_LEG'
|
type BodyPart = 'HEAD' | 'TORSO' | 'LEFT_ARM' | 'RIGHT_ARM' | 'LEFT_LEG' | 'RIGHT_LEG'
|
||||||
type TextureSource = 'skin' | 'wing' | 'cape'
|
type TextureSource = 'skin' | 'displaced-skin' | 'wing' | 'cape'
|
||||||
type TextureRotation = 'NONE' | 'CW' | 'CCW' | 'UPSIDE_DOWN'
|
type TextureRotation = 'NONE' | 'CW' | 'CCW' | 'UPSIDE_DOWN'
|
||||||
type TextureFlip = 'NONE' | 'HORIZONTAL' | 'VERTICAL' | 'BOTH'
|
type TextureFlip = 'NONE' | 'HORIZONTAL' | 'VERTICAL' | 'BOTH'
|
||||||
|
|
||||||
interface EarsFeatures {
|
interface EarsFeatures {
|
||||||
earMode: EarMode
|
earMode: EarMode
|
||||||
earAnchor: EarAnchor
|
earAnchor: EarAnchor
|
||||||
claws: boolean
|
protrusions: Protrusions
|
||||||
horn: boolean
|
|
||||||
tailMode: TailMode
|
tailMode: TailMode
|
||||||
tailSegments: number
|
tailSegments: number
|
||||||
tailBends: [number, number, number, number]
|
tailBends: [number, number, number, number]
|
||||||
@@ -78,9 +91,12 @@ interface EarsFeatures {
|
|||||||
snoutDepth: number
|
snoutDepth: number
|
||||||
chestSize: number
|
chestSize: number
|
||||||
wingMode: WingMode
|
wingMode: WingMode
|
||||||
animateWings: boolean
|
wingAnimationMode: WingAnimationMode
|
||||||
capeEnabled: boolean
|
capeEnabled: boolean
|
||||||
emissive: boolean
|
emissive: boolean
|
||||||
|
legMode: LegMode
|
||||||
|
animateTail: boolean
|
||||||
|
swapJacketBackAndTail: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TexturePair {
|
interface TexturePair {
|
||||||
@@ -150,6 +166,24 @@ const WING_MODES: WingMode[] = [
|
|||||||
'ASYMMETRIC_DUAL',
|
'ASYMMETRIC_DUAL',
|
||||||
'FLAT',
|
'FLAT',
|
||||||
]
|
]
|
||||||
|
const WING_ANIMATION_MODES: WingAnimationMode[] = ['NORMAL', 'NONE', 'NO_FLIGHT']
|
||||||
|
const LEG_MODES: LegMode[] = ['PLANTIGRADE', 'DIGITIGRADE_PARTIAL', 'DIGITIGRADE_FULL']
|
||||||
|
const PROTRUSIONS: Protrusions[] = [
|
||||||
|
'NONE',
|
||||||
|
'HORN',
|
||||||
|
'CLAWS',
|
||||||
|
'CLAWS_AND_HORN',
|
||||||
|
'HALO',
|
||||||
|
'DOUBLE_HALO',
|
||||||
|
'CLAWS_AND_HALO',
|
||||||
|
'CLAWS_AND_DOUBLE_HALO',
|
||||||
|
]
|
||||||
|
const CLAW_PROTRUSIONS = new Set<Protrusions>([
|
||||||
|
'CLAWS',
|
||||||
|
'CLAWS_AND_HORN',
|
||||||
|
'CLAWS_AND_HALO',
|
||||||
|
'CLAWS_AND_DOUBLE_HALO',
|
||||||
|
])
|
||||||
|
|
||||||
const MAGIC_PIXELS = {
|
const MAGIC_PIXELS = {
|
||||||
BLUE: 0x3f23d8,
|
BLUE: 0x3f23d8,
|
||||||
@@ -189,6 +223,54 @@ const FORCED_OPAQUE_REGIONS = [
|
|||||||
[16, 52, 48, 64],
|
[16, 52, 48, 64],
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
const FORCED_OPAQUE_REGIONS_WITHOUT_LEG_BOTTOM = [
|
||||||
|
[8, 0, 24, 8],
|
||||||
|
[0, 8, 32, 16],
|
||||||
|
[4, 16, 8, 20],
|
||||||
|
[20, 16, 36, 20],
|
||||||
|
[44, 16, 52, 20],
|
||||||
|
[0, 20, 16, 38],
|
||||||
|
[16, 20, 56, 32],
|
||||||
|
[20, 48, 24, 52],
|
||||||
|
[36, 48, 44, 52],
|
||||||
|
[16, 52, 32, 58],
|
||||||
|
[32, 52, 48, 64],
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const FORCED_OPAQUE_REGIONS_WITHOUT_LEGS = [
|
||||||
|
[8, 0, 24, 8],
|
||||||
|
[0, 8, 32, 16],
|
||||||
|
[4, 16, 8, 20],
|
||||||
|
[20, 16, 36, 20],
|
||||||
|
[44, 16, 52, 20],
|
||||||
|
[16, 20, 56, 32],
|
||||||
|
[20, 48, 24, 52],
|
||||||
|
[36, 48, 44, 52],
|
||||||
|
[32, 52, 48, 64],
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const LEG_BOTTOM_HALF_REGIONS = [
|
||||||
|
[24, 48, 28, 52, true],
|
||||||
|
[16, 58, 32, 64, true],
|
||||||
|
[8, 16, 12, 20, true],
|
||||||
|
[0, 26, 16, 32, true],
|
||||||
|
[8, 32, 12, 36, false],
|
||||||
|
[0, 42, 16, 48, false],
|
||||||
|
[8, 48, 12, 52, false],
|
||||||
|
[0, 58, 16, 64, false],
|
||||||
|
] as const
|
||||||
|
|
||||||
|
const LEG_REGIONS = [
|
||||||
|
[20, 48, 28, 52, true],
|
||||||
|
[16, 52, 32, 64, true],
|
||||||
|
[4, 16, 12, 20, true],
|
||||||
|
[0, 20, 16, 32, true],
|
||||||
|
[4, 32, 12, 36, false],
|
||||||
|
[0, 36, 16, 48, false],
|
||||||
|
[4, 48, 12, 52, false],
|
||||||
|
[0, 52, 16, 64, false],
|
||||||
|
] as const
|
||||||
|
|
||||||
const BODY_PART_NODES: Record<BodyPart, string> = {
|
const BODY_PART_NODES: Record<BodyPart, string> = {
|
||||||
HEAD: 'Head',
|
HEAD: 'Head',
|
||||||
TORSO: 'Body',
|
TORSO: 'Body',
|
||||||
@@ -303,18 +385,18 @@ function parseV1Features(data: Uint8ClampedArray): EarsFeatures | null {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const reader = new BitReader(new Uint8Array(bytes))
|
const reader = new BitReader(new Uint8Array(bytes))
|
||||||
reader.read(8)
|
const version = reader.read(8)
|
||||||
|
|
||||||
const ears = reader.read(6)
|
const ears = reader.read(6)
|
||||||
const earMode = ears === 0 ? 'NONE' : (EAR_MODES[Math.floor((ears - 1) / 3) + 1] ?? 'NONE')
|
const earMode = ears === 0 ? 'NONE' : (EAR_MODES[Math.floor((ears - 1) / 3) + 1] ?? 'NONE')
|
||||||
const earAnchor = ears === 0 ? 'CENTER' : (EAR_ANCHORS[(ears - 1) % 3] ?? 'CENTER')
|
const earAnchor = ears === 0 ? 'CENTER' : (EAR_ANCHORS[(ears - 1) % 3] ?? 'CENTER')
|
||||||
const claws = reader.readBoolean()
|
let protrusionsIndex = reader.read(2)
|
||||||
const horn = reader.readBoolean()
|
const tailIndex = reader.read(3)
|
||||||
const tailMode = TAIL_MODES[reader.read(3)] ?? 'NONE'
|
let tailMode = TAIL_MODES[tailIndex] ?? 'NONE'
|
||||||
let tailSegments = 0
|
let tailSegments = 0
|
||||||
const tailBends: [number, number, number, number] = [0, 0, 0, 0]
|
const tailBends: [number, number, number, number] = [0, 0, 0, 0]
|
||||||
|
|
||||||
if (tailMode !== 'NONE') {
|
if (tailIndex !== 0) {
|
||||||
tailSegments = reader.read(2) + 1
|
tailSegments = reader.read(2) + 1
|
||||||
for (let index = 0; index < tailSegments; index++) {
|
for (let index = 0; index < tailSegments; index++) {
|
||||||
tailBends[index] = reader.readSignedUnit(6) * 90
|
tailBends[index] = reader.readSignedUnit(6) * 90
|
||||||
@@ -336,12 +418,30 @@ function parseV1Features(data: Uint8ClampedArray): EarsFeatures | null {
|
|||||||
const animateWings = wingMode === 'NONE' ? false : reader.readBoolean()
|
const animateWings = wingMode === 'NONE' ? false : reader.readBoolean()
|
||||||
const capeEnabled = reader.readBoolean()
|
const capeEnabled = reader.readBoolean()
|
||||||
const emissive = reader.readBoolean()
|
const emissive = reader.readBoolean()
|
||||||
|
let wingAnimationMode: WingAnimationMode = animateWings ? 'NORMAL' : 'NONE'
|
||||||
|
let legMode: LegMode = 'PLANTIGRADE'
|
||||||
|
let animateTail = true
|
||||||
|
let swapJacketBackAndTail = false
|
||||||
|
|
||||||
|
if (version >= 1 && tailIndex === 7) {
|
||||||
|
tailMode = TAIL_MODES[tailIndex + reader.read(3)] ?? 'NONE'
|
||||||
|
}
|
||||||
|
if (version >= 2) {
|
||||||
|
legMode = LEG_MODES[reader.read(3)] ?? 'PLANTIGRADE'
|
||||||
|
if (wingMode !== 'NONE') {
|
||||||
|
wingAnimationMode = WING_ANIMATION_MODES[reader.read(3)] ?? wingAnimationMode
|
||||||
|
}
|
||||||
|
animateTail = reader.readBoolean()
|
||||||
|
swapJacketBackAndTail = reader.readBoolean()
|
||||||
|
}
|
||||||
|
if (version >= 3) {
|
||||||
|
protrusionsIndex |= reader.read(2) << 2
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
earMode,
|
earMode,
|
||||||
earAnchor,
|
earAnchor,
|
||||||
claws,
|
protrusions: PROTRUSIONS[protrusionsIndex] ?? 'NONE',
|
||||||
horn,
|
|
||||||
tailMode,
|
tailMode,
|
||||||
tailSegments,
|
tailSegments,
|
||||||
tailBends,
|
tailBends,
|
||||||
@@ -351,9 +451,12 @@ function parseV1Features(data: Uint8ClampedArray): EarsFeatures | null {
|
|||||||
snoutDepth,
|
snoutDepth,
|
||||||
chestSize,
|
chestSize,
|
||||||
wingMode,
|
wingMode,
|
||||||
animateWings,
|
wingAnimationMode,
|
||||||
capeEnabled,
|
capeEnabled,
|
||||||
emissive,
|
emissive,
|
||||||
|
legMode,
|
||||||
|
animateTail,
|
||||||
|
swapJacketBackAndTail,
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
@@ -406,12 +509,31 @@ function parseV0Features(data: Uint8ClampedArray): EarsFeatures {
|
|||||||
[MAGIC_PIXELS.PURPLE, 'ASYMMETRIC_DUAL'],
|
[MAGIC_PIXELS.PURPLE, 'ASYMMETRIC_DUAL'],
|
||||||
[MAGIC_PIXELS.PURPLE2, 'FLAT'],
|
[MAGIC_PIXELS.PURPLE2, 'FLAT'],
|
||||||
])
|
])
|
||||||
|
const protrusionsByPixel = new Map<number, Protrusions>([
|
||||||
|
[MAGIC_PIXELS.BLUE, 'NONE'],
|
||||||
|
[MAGIC_PIXELS.RED, 'NONE'],
|
||||||
|
[MAGIC_PIXELS.GREEN, 'CLAWS'],
|
||||||
|
[MAGIC_PIXELS.PURPLE, 'HORN'],
|
||||||
|
[MAGIC_PIXELS.CYAN, 'CLAWS_AND_HORN'],
|
||||||
|
[MAGIC_PIXELS.WHITE, 'HALO'],
|
||||||
|
[MAGIC_PIXELS.GRAY, 'DOUBLE_HALO'],
|
||||||
|
[MAGIC_PIXELS.PURPLE2, 'CLAWS_AND_HALO'],
|
||||||
|
[MAGIC_PIXELS.PINK, 'CLAWS_AND_DOUBLE_HALO'],
|
||||||
|
])
|
||||||
|
const wingAnimationModeByPixel = new Map<number, WingAnimationMode>([
|
||||||
|
[MAGIC_PIXELS.BLUE, 'NORMAL'],
|
||||||
|
[MAGIC_PIXELS.RED, 'NONE'],
|
||||||
|
[MAGIC_PIXELS.GREEN, 'NO_FLIGHT'],
|
||||||
|
])
|
||||||
|
const legModeByPixel = new Map<number, LegMode>([
|
||||||
|
[MAGIC_PIXELS.BLUE, 'PLANTIGRADE'],
|
||||||
|
[MAGIC_PIXELS.GREEN, 'DIGITIGRADE_PARTIAL'],
|
||||||
|
[MAGIC_PIXELS.PINK, 'DIGITIGRADE_FULL'],
|
||||||
|
])
|
||||||
|
|
||||||
const earMode = earModeByPixel.get(getConfigPixel(data, 1).rgb) ?? 'NONE'
|
const earMode = earModeByPixel.get(getConfigPixel(data, 1).rgb) ?? 'NONE'
|
||||||
const earAnchor = earAnchorByPixel.get(getConfigPixel(data, 2).rgb) ?? 'CENTER'
|
const earAnchor = earAnchorByPixel.get(getConfigPixel(data, 2).rgb) ?? 'CENTER'
|
||||||
const protrusions = getConfigPixel(data, 3).rgb
|
const protrusions = protrusionsByPixel.get(getConfigPixel(data, 3).rgb) ?? 'NONE'
|
||||||
const claws = protrusions === MAGIC_PIXELS.GREEN || protrusions === MAGIC_PIXELS.CYAN
|
|
||||||
const horn = protrusions === MAGIC_PIXELS.PURPLE || protrusions === MAGIC_PIXELS.CYAN
|
|
||||||
const tailMode = tailModeByPixel.get(getConfigPixel(data, 4).rgb) ?? 'NONE'
|
const tailMode = tailModeByPixel.get(getConfigPixel(data, 4).rgb) ?? 'NONE'
|
||||||
const tailPixel = getConfigPixel(data, 5)
|
const tailPixel = getConfigPixel(data, 5)
|
||||||
const tailBends: [number, number, number, number] = [0, 0, 0, 0]
|
const tailBends: [number, number, number, number] = [0, 0, 0, 0]
|
||||||
@@ -444,12 +566,13 @@ function parseV0Features(data: Uint8ClampedArray): EarsFeatures {
|
|||||||
const etcIsDefault = etcPixel.rgb === MAGIC_PIXELS.BLUE
|
const etcIsDefault = etcPixel.rgb === MAGIC_PIXELS.BLUE
|
||||||
const chestSize = etcIsDefault ? 0 : Math.min(etcPixel.r / 128, 1)
|
const chestSize = etcIsDefault ? 0 : Math.min(etcPixel.r / 128, 1)
|
||||||
const capeEnabled = !etcIsDefault && (etcPixel.b & 16) !== 0
|
const capeEnabled = !etcIsDefault && (etcPixel.b & 16) !== 0
|
||||||
|
const bitflags = getConfigPixel(data, 12).rgb
|
||||||
|
const bitflagsAreDefault = bitflags === MAGIC_PIXELS.BLUE
|
||||||
|
|
||||||
return {
|
return {
|
||||||
earMode,
|
earMode,
|
||||||
earAnchor,
|
earAnchor,
|
||||||
claws,
|
protrusions,
|
||||||
horn,
|
|
||||||
tailMode,
|
tailMode,
|
||||||
tailSegments,
|
tailSegments,
|
||||||
tailBends,
|
tailBends,
|
||||||
@@ -459,9 +582,12 @@ function parseV0Features(data: Uint8ClampedArray): EarsFeatures {
|
|||||||
snoutDepth,
|
snoutDepth,
|
||||||
chestSize,
|
chestSize,
|
||||||
wingMode: wingModeByPixel.get(getConfigPixel(data, 8).rgb) ?? 'NONE',
|
wingMode: wingModeByPixel.get(getConfigPixel(data, 8).rgb) ?? 'NONE',
|
||||||
animateWings: getConfigPixel(data, 9).rgb !== MAGIC_PIXELS.RED,
|
wingAnimationMode: wingAnimationModeByPixel.get(getConfigPixel(data, 9).rgb) ?? 'NORMAL',
|
||||||
capeEnabled,
|
capeEnabled,
|
||||||
emissive: getConfigPixel(data, 10).rgb === MAGIC_PIXELS.ORANGE,
|
emissive: getConfigPixel(data, 10).rgb === MAGIC_PIXELS.ORANGE,
|
||||||
|
legMode: legModeByPixel.get(getConfigPixel(data, 11).rgb) ?? 'PLANTIGRADE',
|
||||||
|
animateTail: bitflagsAreDefault || (bitflags & 1) !== 0,
|
||||||
|
swapJacketBackAndTail: !bitflagsAreDefault && (bitflags & 2) !== 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,8 +693,14 @@ function applyEraseData(data: Uint8ClampedArray, eraseData: Uint8Array | undefin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function forceOpaqueSkinRegions(data: Uint8ClampedArray) {
|
function forceOpaqueSkinRegions(data: Uint8ClampedArray, legMode: LegMode) {
|
||||||
for (const [x1, y1, x2, y2] of FORCED_OPAQUE_REGIONS) {
|
const regions =
|
||||||
|
legMode === 'DIGITIGRADE_FULL'
|
||||||
|
? FORCED_OPAQUE_REGIONS_WITHOUT_LEGS
|
||||||
|
: legMode === 'DIGITIGRADE_PARTIAL'
|
||||||
|
? FORCED_OPAQUE_REGIONS_WITHOUT_LEG_BOTTOM
|
||||||
|
: FORCED_OPAQUE_REGIONS
|
||||||
|
for (const [x1, y1, x2, y2] of regions) {
|
||||||
for (let x = x1; x < x2; x++) {
|
for (let x = x1; x < x2; x++) {
|
||||||
for (let y = y1; y < y2; y++) {
|
for (let y = y1; y < y2; y++) {
|
||||||
data[pixelOffset(x, y) + 3] = 255
|
data[pixelOffset(x, y) + 3] = 255
|
||||||
@@ -577,6 +709,44 @@ function forceOpaqueSkinRegions(data: Uint8ClampedArray) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function displaceDigitigradeLegPixels(data: Uint8ClampedArray, legMode: LegMode) {
|
||||||
|
if (legMode === 'PLANTIGRADE') return null
|
||||||
|
|
||||||
|
const displaced = new Uint8ClampedArray(data.length)
|
||||||
|
const regions = legMode === 'DIGITIGRADE_FULL' ? LEG_REGIONS : LEG_BOTTOM_HALF_REGIONS
|
||||||
|
for (const [x1, y1, x2, y2, forceOpaque] of regions) {
|
||||||
|
for (let x = x1; x < x2; x++) {
|
||||||
|
for (let y = y1; y < y2; y++) {
|
||||||
|
const offset = pixelOffset(x, y)
|
||||||
|
displaced.set(data.subarray(offset, offset + 4), offset)
|
||||||
|
if (forceOpaque) displaced[offset + 3] = 255
|
||||||
|
data.fill(0, offset, offset + 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return displaced
|
||||||
|
}
|
||||||
|
|
||||||
|
function swapJacketBackAndTailPixels(data: Uint8ClampedArray) {
|
||||||
|
const tail = new Uint8ClampedArray(8 * 12 * 4)
|
||||||
|
for (let x = 0; x < 8; x++) {
|
||||||
|
for (let y = 0; y < 12; y++) {
|
||||||
|
const tailOffset = pixelOffset(56 + x, 16 + y)
|
||||||
|
const savedOffset = (y * 8 + x) * 4
|
||||||
|
tail.set(data.subarray(tailOffset, tailOffset + 4), savedOffset)
|
||||||
|
|
||||||
|
const jacketOffset = pixelOffset(32 + x, 36 + (11 - y))
|
||||||
|
data.set(data.subarray(jacketOffset, jacketOffset + 4), tailOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let x = 0; x < 8; x++) {
|
||||||
|
for (let y = 0; y < 12; y++) {
|
||||||
|
const savedOffset = (y * 8 + x) * 4
|
||||||
|
data.set(tail.subarray(savedOffset, savedOffset + 4), pixelOffset(32 + x, 36 + y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getEmissivePalette(data: Uint8ClampedArray) {
|
function getEmissivePalette(data: Uint8ClampedArray) {
|
||||||
const palette = new Set<number>()
|
const palette = new Set<number>()
|
||||||
for (let x = 52; x < 56; x++) {
|
for (let x = 52; x < 56; x++) {
|
||||||
@@ -857,7 +1027,22 @@ class EarsGeometryBuilder {
|
|||||||
flip: TextureFlip = 'NONE',
|
flip: TextureFlip = 'NONE',
|
||||||
grow = 0,
|
grow = 0,
|
||||||
) {
|
) {
|
||||||
this.quad(u, v, width, height, rotation, flip, grow, false)
|
this.quad(u, v, width, height, rotation, flip, grow, false, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
frontSkew(
|
||||||
|
u: number,
|
||||||
|
v: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
xSkew: number,
|
||||||
|
ySkew: number,
|
||||||
|
zSkew: number,
|
||||||
|
rotation: TextureRotation = 'NONE',
|
||||||
|
flip: TextureFlip = 'NONE',
|
||||||
|
grow = 0,
|
||||||
|
) {
|
||||||
|
this.quad(u, v, width, height, rotation, flip, grow, false, xSkew, ySkew, zSkew)
|
||||||
}
|
}
|
||||||
|
|
||||||
back(
|
back(
|
||||||
@@ -869,7 +1054,7 @@ class EarsGeometryBuilder {
|
|||||||
flip: TextureFlip = 'NONE',
|
flip: TextureFlip = 'NONE',
|
||||||
grow = 0,
|
grow = 0,
|
||||||
) {
|
) {
|
||||||
this.quad(u, v, width, height, rotation, flipHorizontally(flip), grow, true)
|
this.quad(u, v, width, height, rotation, flipHorizontally(flip), grow, true, 0, 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
doubleSided(
|
doubleSided(
|
||||||
@@ -905,42 +1090,46 @@ class EarsGeometryBuilder {
|
|||||||
flip: TextureFlip,
|
flip: TextureFlip,
|
||||||
grow: number,
|
grow: number,
|
||||||
back: boolean,
|
back: boolean,
|
||||||
|
xSkew: number,
|
||||||
|
ySkew: number,
|
||||||
|
zSkew: number,
|
||||||
) {
|
) {
|
||||||
const anchor = this.state.anchor
|
const anchor = this.state.anchor
|
||||||
const textures = this.textures.get(this.textureSource)
|
const textures = this.textures.get(this.textureSource)
|
||||||
if (!anchor || !textures) return
|
if (!anchor || !textures) return
|
||||||
|
|
||||||
const textureWidth = this.textureSource === 'skin' ? 64 : 20
|
const skinTexture = this.textureSource === 'skin' || this.textureSource === 'displaced-skin'
|
||||||
const textureHeight = this.textureSource === 'skin' ? 64 : 16
|
const textureWidth = skinTexture ? 64 : 20
|
||||||
|
const textureHeight = skinTexture ? 64 : 16
|
||||||
const uvs = calculateUvs(u, v, width, height, rotation, flip, textureWidth, textureHeight)
|
const uvs = calculateUvs(u, v, width, height, rotation, flip, textureWidth, textureHeight)
|
||||||
const positions = back
|
const positions = back
|
||||||
? [
|
? [
|
||||||
|
grow + xSkew,
|
||||||
|
grow + ySkew,
|
||||||
|
-zSkew,
|
||||||
|
-width - grow + xSkew,
|
||||||
grow,
|
grow,
|
||||||
grow,
|
-zSkew,
|
||||||
0,
|
-width - grow - xSkew,
|
||||||
-width - grow,
|
|
||||||
grow,
|
|
||||||
0,
|
|
||||||
-width - grow,
|
|
||||||
-height - grow,
|
-height - grow,
|
||||||
0,
|
zSkew,
|
||||||
grow,
|
grow - xSkew,
|
||||||
-height - grow,
|
-height - grow + ySkew,
|
||||||
0,
|
zSkew,
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
grow,
|
grow - xSkew,
|
||||||
-height - grow,
|
-height - grow + ySkew,
|
||||||
0,
|
zSkew,
|
||||||
-width - grow,
|
-width - grow - xSkew,
|
||||||
-height - grow,
|
-height - grow - ySkew,
|
||||||
0,
|
zSkew,
|
||||||
-width - grow,
|
-width - grow + xSkew,
|
||||||
grow,
|
grow - ySkew,
|
||||||
0,
|
-zSkew,
|
||||||
grow,
|
grow + xSkew,
|
||||||
grow,
|
grow + ySkew,
|
||||||
0,
|
-zSkew,
|
||||||
]
|
]
|
||||||
const orderedUvs = back ? [uvs[3], uvs[2], uvs[1], uvs[0]] : [uvs[0], uvs[1], uvs[2], uvs[3]]
|
const orderedUvs = back ? [uvs[3], uvs[2], uvs[1], uvs[0]] : [uvs[0], uvs[1], uvs[2], uvs[3]]
|
||||||
const geometry = new THREE.BufferGeometry()
|
const geometry = new THREE.BufferGeometry()
|
||||||
@@ -1213,18 +1402,24 @@ function renderTail(builder: EarsGeometryBuilder, features: EarsFeatures) {
|
|||||||
builder.pop()
|
builder.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderClawsAndHorn(builder: EarsGeometryBuilder, features: EarsFeatures, slim: boolean) {
|
function renderProtrusions(builder: EarsGeometryBuilder, features: EarsFeatures, slim: boolean) {
|
||||||
if (features.claws) {
|
if (CLAW_PROTRUSIONS.has(features.protrusions)) {
|
||||||
|
const legOffset =
|
||||||
|
features.legMode === 'DIGITIGRADE_FULL'
|
||||||
|
? -1.5
|
||||||
|
: features.legMode === 'DIGITIGRADE_PARTIAL'
|
||||||
|
? -1
|
||||||
|
: 0
|
||||||
builder.push()
|
builder.push()
|
||||||
builder.anchorTo('LEFT_LEG')
|
builder.anchorTo('LEFT_LEG')
|
||||||
builder.translate(0, 0, -4)
|
builder.translate(0, 0, -4 + legOffset)
|
||||||
builder.rotate(90, 1, 0, 0)
|
builder.rotate(90, 1, 0, 0)
|
||||||
builder.doubleSided(16, 48, 4, 4, 'NONE', 'HORIZONTAL')
|
builder.doubleSided(16, 48, 4, 4, 'NONE', 'HORIZONTAL')
|
||||||
builder.pop()
|
builder.pop()
|
||||||
|
|
||||||
builder.push()
|
builder.push()
|
||||||
builder.anchorTo('RIGHT_LEG')
|
builder.anchorTo('RIGHT_LEG')
|
||||||
builder.translate(0, 0, -4)
|
builder.translate(0, 0, -4 + legOffset)
|
||||||
builder.rotate(90, 1, 0, 0)
|
builder.rotate(90, 1, 0, 0)
|
||||||
builder.doubleSided(0, 16, 4, 4, 'NONE', 'HORIZONTAL')
|
builder.doubleSided(0, 16, 4, 4, 'NONE', 'HORIZONTAL')
|
||||||
builder.pop()
|
builder.pop()
|
||||||
@@ -1244,7 +1439,7 @@ function renderClawsAndHorn(builder: EarsGeometryBuilder, features: EarsFeatures
|
|||||||
builder.pop()
|
builder.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (features.horn) {
|
if (features.protrusions === 'HORN' || features.protrusions === 'CLAWS_AND_HORN') {
|
||||||
builder.push()
|
builder.push()
|
||||||
builder.anchorTo('HEAD')
|
builder.anchorTo('HEAD')
|
||||||
builder.translate(0, -8, 0)
|
builder.translate(0, -8, 0)
|
||||||
@@ -1253,6 +1448,172 @@ function renderClawsAndHorn(builder: EarsGeometryBuilder, features: EarsFeatures
|
|||||||
builder.doubleSided(56, 0, 8, 8)
|
builder.doubleSided(56, 0, 8, 8)
|
||||||
builder.pop()
|
builder.pop()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (features.protrusions === 'HALO' || features.protrusions === 'CLAWS_AND_HALO') {
|
||||||
|
builder.push()
|
||||||
|
builder.anchorTo('HEAD')
|
||||||
|
builder.translate(0, -12, 0)
|
||||||
|
builder.rotate(90, 1, 0, 0)
|
||||||
|
builder.doubleSided(56, 0, 8, 8)
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (features.protrusions === 'DOUBLE_HALO' || features.protrusions === 'CLAWS_AND_DOUBLE_HALO') {
|
||||||
|
builder.push()
|
||||||
|
builder.anchorTo('HEAD')
|
||||||
|
builder.translate(0, -10, 0)
|
||||||
|
builder.rotate(90, 1, 0, 0)
|
||||||
|
for (let index = 0; index < 2; index++) {
|
||||||
|
builder.translate(4, 4, 0)
|
||||||
|
builder.rotate(45, 0, 0, 1)
|
||||||
|
builder.translate(-4, -4, 2)
|
||||||
|
builder.doubleSided(56, 0, 8, 8)
|
||||||
|
}
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawDigitigradeLeg(
|
||||||
|
builder: EarsGeometryBuilder,
|
||||||
|
u: number,
|
||||||
|
v: number,
|
||||||
|
grow: number,
|
||||||
|
bottom: boolean,
|
||||||
|
mend: boolean,
|
||||||
|
) {
|
||||||
|
const width = 4
|
||||||
|
const height = 12
|
||||||
|
const depth = 4
|
||||||
|
const doubleGrow = grow * 2
|
||||||
|
const skew = 1
|
||||||
|
const vOffset = bottom ? 10 : 4
|
||||||
|
|
||||||
|
builder.push()
|
||||||
|
builder.translate(width / 2, height / 2, depth / 2)
|
||||||
|
builder.scale(
|
||||||
|
(width + doubleGrow) / width,
|
||||||
|
(height + doubleGrow) / height,
|
||||||
|
(depth + doubleGrow) / depth,
|
||||||
|
)
|
||||||
|
builder.translate(-width / 2, -height / 2, -depth / 2)
|
||||||
|
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, bottom ? -6 : -12, 0)
|
||||||
|
if (!mend && bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, 2, 0)
|
||||||
|
builder.frontSkew(u + 4, v + vOffset + 2, 4, 4, 0, 0, -skew)
|
||||||
|
builder.pop()
|
||||||
|
} else {
|
||||||
|
builder.frontSkew(u + 4, v + vOffset, 4, 6, 0, 0, -skew)
|
||||||
|
}
|
||||||
|
if (mend && !bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, 6, 0)
|
||||||
|
builder.frontSkew(u + 4, v + vOffset + 6, 4, 2, 0, 0, skew)
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.rotate(-90, 0, 1, 0)
|
||||||
|
builder.translate(0, 0, -4)
|
||||||
|
if (!mend && bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, 2, 0)
|
||||||
|
builder.frontSkew(u + 8, v + vOffset + 2, 4, 4, -skew, 0, 0)
|
||||||
|
builder.pop()
|
||||||
|
} else {
|
||||||
|
builder.frontSkew(u + 8, v + vOffset, 4, 6, -skew, 0, 0)
|
||||||
|
}
|
||||||
|
if (mend && !bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, 6, 0)
|
||||||
|
builder.frontSkew(u + 8, v + vOffset + 6, 4, 2, skew, 0, 0)
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.rotate(-90, 0, 1, 0)
|
||||||
|
builder.translate(0, 0, -4)
|
||||||
|
if (!mend && bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, 2, 0)
|
||||||
|
builder.frontSkew(u + 12, v + vOffset + 2, 4, 4, 0, 0, skew)
|
||||||
|
builder.pop()
|
||||||
|
} else {
|
||||||
|
builder.frontSkew(u + 12, v + vOffset, 4, 6, 0, 0, skew)
|
||||||
|
}
|
||||||
|
if (mend && !bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, 6, 0)
|
||||||
|
builder.frontSkew(u + 12, v + vOffset + 5, 4, 2, 0, 0, -skew)
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.rotate(-90, 0, 1, 0)
|
||||||
|
builder.translate(0, 0, -4)
|
||||||
|
if (!mend && bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, 2, 0)
|
||||||
|
builder.frontSkew(u, v + vOffset + 2, 4, 4, skew, 0, 0)
|
||||||
|
builder.pop()
|
||||||
|
} else {
|
||||||
|
builder.frontSkew(u, v + vOffset, 4, 6, skew, 0, 0)
|
||||||
|
}
|
||||||
|
if (mend && !bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, 6, 0)
|
||||||
|
builder.frontSkew(u, v + vOffset + 6, 4, 2, -skew, 0, 0)
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
builder.pop()
|
||||||
|
|
||||||
|
builder.push()
|
||||||
|
builder.rotate(90, 1, 0, 0)
|
||||||
|
if (bottom) {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, -skew, 0)
|
||||||
|
builder.front(u + 8, v, 4, 4, 'NONE', 'VERTICAL')
|
||||||
|
builder.pop()
|
||||||
|
} else {
|
||||||
|
builder.push()
|
||||||
|
builder.translate(0, skew, 12)
|
||||||
|
builder.back(u + 8, v, 4, 4, 'NONE', 'VERTICAL')
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
if (mend && bottom) {
|
||||||
|
builder.translate(0, 0, 6)
|
||||||
|
builder.front(u + 4, v + vOffset, 4, 1)
|
||||||
|
builder.translate(0, 4, 0)
|
||||||
|
builder.back(u + 12, v + vOffset, 4, 1)
|
||||||
|
}
|
||||||
|
builder.pop()
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDigitigradeLegs(builder: EarsGeometryBuilder, features: EarsFeatures) {
|
||||||
|
if (features.legMode === 'PLANTIGRADE') return
|
||||||
|
|
||||||
|
const full = features.legMode === 'DIGITIGRADE_FULL'
|
||||||
|
builder.bind('displaced-skin')
|
||||||
|
for (let layer = 0; layer < 2; layer++) {
|
||||||
|
const grow = layer === 0 ? 0 : 0.25
|
||||||
|
|
||||||
|
builder.push()
|
||||||
|
builder.anchorTo('LEFT_LEG')
|
||||||
|
if (full) builder.translate(0, 0, -0.5)
|
||||||
|
if (layer === 1) builder.translate(0, 0.5, 0)
|
||||||
|
if (full) drawDigitigradeLeg(builder, layer === 0 ? 16 : 0, 48, grow, false, true)
|
||||||
|
drawDigitigradeLeg(builder, layer === 0 ? 16 : 0, 48, grow, true, !full)
|
||||||
|
builder.pop()
|
||||||
|
|
||||||
|
builder.push()
|
||||||
|
builder.anchorTo('RIGHT_LEG')
|
||||||
|
if (full) builder.translate(0, 0, -0.5)
|
||||||
|
if (layer === 1) builder.translate(0, 0.5, 0)
|
||||||
|
if (full) drawDigitigradeLeg(builder, 0, layer === 0 ? 16 : 32, grow, false, true)
|
||||||
|
drawDigitigradeLeg(builder, 0, layer === 0 ? 16 : 32, grow, true, !full)
|
||||||
|
builder.pop()
|
||||||
|
}
|
||||||
|
builder.bind('skin')
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSnout(builder: EarsGeometryBuilder, features: EarsFeatures) {
|
function renderSnout(builder: EarsGeometryBuilder, features: EarsFeatures) {
|
||||||
@@ -1366,7 +1727,7 @@ function renderWings(builder: EarsGeometryBuilder, features: EarsFeatures) {
|
|||||||
const mode = features.wingMode
|
const mode = features.wingMode
|
||||||
if (mode === 'NONE') return
|
if (mode === 'NONE') return
|
||||||
|
|
||||||
const wiggle = features.animateWings ? Math.sin(8 / 12) * 2 : 0
|
const wiggle = features.wingAnimationMode !== 'NONE' ? Math.sin(8 / 12) * 2 : 0
|
||||||
builder.push()
|
builder.push()
|
||||||
builder.anchorTo('TORSO')
|
builder.anchorTo('TORSO')
|
||||||
builder.bind('wing')
|
builder.bind('wing')
|
||||||
@@ -1604,7 +1965,9 @@ export function applyEarsMod(model: THREE.Object3D, sourceTexture: THREE.Texture
|
|||||||
const alfalfa = parseAlfalfa(sourceImage.data)
|
const alfalfa = parseAlfalfa(sourceImage.data)
|
||||||
const processedData = new Uint8ClampedArray(sourceImage.data)
|
const processedData = new Uint8ClampedArray(sourceImage.data)
|
||||||
applyEraseData(processedData, alfalfa.get('erase'))
|
applyEraseData(processedData, alfalfa.get('erase'))
|
||||||
forceOpaqueSkinRegions(processedData)
|
const displacedData = displaceDigitigradeLegPixels(processedData, features.legMode)
|
||||||
|
if (features.swapJacketBackAndTail) swapJacketBackAndTailPixels(processedData)
|
||||||
|
forceOpaqueSkinRegions(processedData, features.legMode)
|
||||||
|
|
||||||
const palette = features.emissive ? getEmissivePalette(processedData) : new Set<number>()
|
const palette = features.emissive ? getEmissivePalette(processedData) : new Set<number>()
|
||||||
if (palette.size === 0) features.emissive = false
|
if (palette.size === 0) features.emissive = false
|
||||||
@@ -1623,6 +1986,20 @@ export function applyEarsMod(model: THREE.Object3D, sourceTexture: THREE.Texture
|
|||||||
if (skinTextures.emissive) addEmissiveBodyOverlays(model, skinTextures.emissive, resources)
|
if (skinTextures.emissive) addEmissiveBodyOverlays(model, skinTextures.emissive, resources)
|
||||||
|
|
||||||
const textures = new Map<TextureSource, TexturePair>([['skin', skinTextures]])
|
const textures = new Map<TextureSource, TexturePair>([['skin', skinTextures]])
|
||||||
|
if (displacedData) {
|
||||||
|
const displacedPixels = features.emissive
|
||||||
|
? splitEmissivePixels(displacedData, palette)
|
||||||
|
: { base: displacedData, emissive: undefined }
|
||||||
|
const displacedTextures: TexturePair = {
|
||||||
|
base: createCanvasTexture(displacedPixels.base, SKIN_SIZE, SKIN_SIZE),
|
||||||
|
emissive: displacedPixels.emissive
|
||||||
|
? createCanvasTexture(displacedPixels.emissive, SKIN_SIZE, SKIN_SIZE)
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
resources.featureTextures.add(displacedTextures.base)
|
||||||
|
if (displacedTextures.emissive) resources.featureTextures.add(displacedTextures.emissive)
|
||||||
|
textures.set('displaced-skin', displacedTextures)
|
||||||
|
}
|
||||||
const wingData = alfalfa.get('wing')
|
const wingData = alfalfa.get('wing')
|
||||||
const wingDimensions = wingData ? readPngDimensions(wingData) : null
|
const wingDimensions = wingData ? readPngDimensions(wingData) : null
|
||||||
if (
|
if (
|
||||||
@@ -1659,10 +2036,11 @@ export function applyEarsMod(model: THREE.Object3D, sourceTexture: THREE.Texture
|
|||||||
const builder = new EarsGeometryBuilder(anchors, textures, resources)
|
const builder = new EarsGeometryBuilder(anchors, textures, resources)
|
||||||
renderEars(builder, features)
|
renderEars(builder, features)
|
||||||
renderTail(builder, features)
|
renderTail(builder, features)
|
||||||
renderClawsAndHorn(builder, features, isSlimModel(model))
|
renderProtrusions(builder, features, isSlimModel(model))
|
||||||
renderSnout(builder, features)
|
renderSnout(builder, features)
|
||||||
renderChest(builder, features)
|
renderChest(builder, features)
|
||||||
renderWings(builder, features)
|
renderWings(builder, features)
|
||||||
renderCape(builder, features)
|
renderCape(builder, features)
|
||||||
|
renderDigitigradeLegs(builder, features)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user