Skip to content

Commit

Permalink
Move trapezoid rendering out of the decoder, remove byte-wrap hack
Browse files Browse the repository at this point in the history
  • Loading branch information
jeremypenner committed Feb 2, 2024
1 parent 990364d commit 57722d5
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 141 deletions.
91 changes: 1 addition & 90 deletions inspector/codec.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,104 +222,15 @@ celDecoder.trap = (data, cel) => {
i ++
}
}
// dline.m:132: ; convert wild color to blue
// you can't have a trapezoid with a texture _and_ a pattern
cel.colorOverrides = { pattern: 15 }
}

cel.raw = {
width: cel.width,
x1a: data.getUint8(7),
x1b: data.getUint8(8),
x2a: data.getUint8(9),
x2b: data.getUint8(10)
}
cel.x1a = cel.raw.x1a
cel.x1b = cel.raw.x1b
cel.x2a = cel.raw.x2a
cel.x2b = cel.raw.x2b
if (cel.x1b < cel.x1a) { cel.x1b += 256 }
if (cel.x2b < cel.x2a) { cel.x2b += 256 }
cel.xCorrection = Math.floor(Math.min(cel.x1a, cel.x2a) / 4)
cel.x1a -= cel.xCorrection * 4
cel.x1b -= cel.xCorrection * 4
cel.x2a -= cel.xCorrection * 4
cel.x2b -= cel.xCorrection * 4

// trapezoid-drawing algorithm:
// draw_line: draws a line from x1a,y1 to x1b, y1
// handles border drawing (last/first line, edges)
// decreases vcount, then jumps to cycle1 if there
// are more lines
// cycle1: run bresenham, determine if x1a (left edge) needs to be incremented
// or decremented (self-modifying code! the instruction in inc_dec1 is
// written at trap.m:52)
// has logic to jump back to cycle1 if we have a sharp enough angle that
// we need to move more than one pixel horizontally
// cycle2: same thing, but for x2a (right edge)
// at the end, increments y1 and jumps back to the top of draw_line
cel.width = Math.floor(Math.max(cel.x1a, cel.x1b, cel.x2a, cel.x2b) / 4) + 1
// trap.m:32 - delta_y and vcount are calculated by subtracting y2 - y1.
// mix.m:253: y2 is calculated as cel_y + cel_height
// mix.m:261: y1 is calculated as cel_y + 1
// So for a one-pixel tall trapezoid, deltay is 0, because y1 == y2.
// vcount is decremented until it reaches -1, compensating for the off-by-one.
const deltay = cel.height - 1
cel.bitmap = emptyBitmap(cel.width, cel.height)
const dxa = Math.abs(cel.x1a - cel.x2a)
const dxb = Math.abs(cel.x1b - cel.x2b)
const countMaxA = Math.max(dxa, deltay)
const countMaxB = Math.max(dxb, deltay)
const inca = cel.x1a < cel.x2a ? 1 : -1
const incb = cel.x1b < cel.x2b ? 1 : -1
let x1aLo = Math.floor(countMaxA / 2)
let y1aLo = x1aLo
let x1bLo = Math.floor(countMaxB / 2)
let y1bLo = x1bLo
let xa = cel.x1a
let xb = cel.x1b
for (let y = 0; y < cel.height; y ++) {
const line = cel.bitmap[y]
if (cel.border && (y == 0 || y == (cel.height - 1))) {
// top and bottom border line
horizontalLine(cel.bitmap, xa, xb, y, 0xaa, true)
} else {
if (cel.texture) {
const texLine = cel.texture[y % cel.texture.length]
for (let x = xa; x <= xb; x ++) {
line[x] = texLine[x % texLine.length]
}
} else {
horizontalLine(cel.bitmap, xa, xb, y, cel.pattern, cel.border)
}
}

if (cel.border) {
line[xa] = 2
line[xb] = 2
}

// cycle1: move xa
do {
x1aLo += dxa
if (x1aLo >= countMaxA) {
x1aLo -= countMaxA
xa += inca
}
y1aLo += deltay
} while (y1aLo < countMaxA)
y1aLo -= countMaxA

// cycle2: move xb
do {
x1bLo += dxb
if (x1bLo >= countMaxB) {
x1bLo -= countMaxB
xb += incb
}
y1bLo += deltay
} while (y1bLo < countMaxA)
y1bLo -= countMaxA
}
}

celDecoder.text = (data, cel) => {
Expand Down
51 changes: 18 additions & 33 deletions inspector/region.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,22 +103,6 @@ export const propFromMod = (mod, ref) => {
// not ready to parse yet
return null
}
// Trapezoid positioning hack:
// Shape-drawing code on the C64 always works in a context where the coordinates have
// been pre-transformed to the final position of the shape. There are numerous cases
// in the server data where the trapezoid shape data combined with the actual position
// of the object causes the byte holding the X coordinates to overflow and wrap back to
// zero.
// Our renderer was built with the asssumption that it is possible to render a cel in
// isolation, and composite it into the scene afterwards. But this is not true for
// trapezoids that leverage this overflow behaviour; the data doesn't really make sense
// unless you add the offset to it. So for server-defined shapes, we cheat, and pre-
// transform the shape data so that it's the same as what the C64 code would be using
// when rendering the scene, and then, when compositing, always position trapezoid
// objects at X position 0 (see propLocationFromObjectXY).
// We clear the lower 2 bits of the mod's X position, as the C64 renderer always shifts
// it into "Habitat space" to line up the object on a byte boundary.
const offsetX = (xOffset) => ((mod.x & 0xfc) + xOffset) % 256
const classname = javaTypeToMuddleClass(mod.type)
let fnAugment = null
if (classname == "class_super_trapezoid" && mod.pattern) {
Expand All @@ -129,10 +113,10 @@ export const propFromMod = (mod, ref) => {
superdata.set(mod.pattern, data.byteLength + 2)
const trapview = new DataView(superdata.buffer)
trapview.setUint8(celoff + 1, mod.height)
trapview.setUint8(celoff + 7, offsetX(mod.upper_left_x))
trapview.setUint8(celoff + 8, offsetX(mod.upper_right_x))
trapview.setUint8(celoff + 9, offsetX(mod.lower_left_x))
trapview.setUint8(celoff + 10, offsetX(mod.lower_right_x))
trapview.setUint8(celoff + 7, mod.upper_left_x)
trapview.setUint8(celoff + 8, mod.upper_right_x)
trapview.setUint8(celoff + 9, mod.lower_left_x)
trapview.setUint8(celoff + 10, mod.lower_right_x)
trapview.setUint8(celoff + 11, mod.pattern_x_size)
trapview.setUint8(celoff + 12, mod.pattern_y_size)
return trapview
Expand All @@ -144,10 +128,10 @@ export const propFromMod = (mod, ref) => {
const celoff = data.getUint16(7 + celCount + (icel * 2), true)
data.setUint8(celoff + 1, mod.height)
if (icel == 0) {
data.setUint8(celoff + 7, offsetX(mod.upper_left_x))
data.setUint8(celoff + 8, offsetX(mod.upper_right_x))
data.setUint8(celoff + 9, offsetX(mod.lower_left_x))
data.setUint8(celoff + 10, offsetX(mod.lower_right_x))
data.setUint8(celoff + 7, mod.upper_left_x)
data.setUint8(celoff + 8, mod.upper_right_x)
data.setUint8(celoff + 9, mod.lower_left_x)
data.setUint8(celoff + 10, mod.lower_right_x)
}
}
return data
Expand All @@ -160,8 +144,8 @@ const signedXCoordinate = (modX) => modX > 208 ? signedByte(modX) : modX
const zIndexFromObjectY = (modY) => modY > 127 ? (128 + (256 - modY)) : modY
const objectZComparitor = (obj1, obj2) => zIndexFromObjectY(obj1.mods[0].y) - zIndexFromObjectY(obj2.mods[0].y)

const propLocationFromObjectXY = (prop, modX, modY) => {
return [prop.isTrap ? 0 : Math.floor(signedXCoordinate(modX) / 4), modY % 128, zIndexFromObjectY(modY)]
const propLocationFromObjectXY = (modX, modY) => {
return [Math.floor(signedXCoordinate(modX) / 4), modY % 128, zIndexFromObjectY(modY)]
}

const colorsFromMod = (mod) => {
Expand All @@ -177,8 +161,9 @@ const colorsFromMod = (mod) => {
return colors
}

export const propFramesFromMod = (prop, mod, flipOverride = null) => {
export const propFramesFromMod = (prop, mod, xOrigin = 0, flipOverride = null) => {
const colors = colorsFromMod(mod)
colors.xOrigin = xOrigin
const flipHorizontal = flipOverride ?? ((mod.orientation ?? 0) & 0x01) != 0
const grState = mod.gr_state ?? 0
if (prop.animations.length > 0) {
Expand Down Expand Up @@ -237,18 +222,18 @@ export const objectSpaceFromLayout = ({ x, y, frames }) =>
translateSpace(compositeSpaces(frames), x, y)

export const containedItemLayout = (prop, mod, containerProp, containerMod, containerSpace) => {
const [containerX, containerY, containerZ] = propLocationFromObjectXY(containerProp, containerMod.x, containerMod.y)
const [containerX, containerY, containerZ] = propLocationFromObjectXY(containerMod.x, containerMod.y)
const { x: offsetX, y: offsetY } = offsetsFromContainer(containerProp, containerMod, mod)
// offsets are relative to `cel_x_origin` / `cel_y_origin`, which is in "habitat space" but with
// the y axis inverted (see render.m:115-121)
const flipHorizontal = (containerMod.orientation & 0x01) != 0
const frames = propFramesFromMod(prop, mod, flipHorizontal)
// if the contents are drawn in front, the container has its origin offset by the offset of its first cel.
const originX = containerProp.contentsInFront ? containerSpace.xOrigin : 0
const originY = containerProp.contentsInFront ? containerSpace.yOrigin : 0
const x = (containerX - originX) + (flipHorizontal ? -offsetX : offsetX)
const y = containerY - (offsetY + originY)
const z = containerZ
// offsets are relative to `cel_x_origin` / `cel_y_origin`, which is in "habitat space" but with
// the y axis inverted (see render.m:115-121)
const frames = propFramesFromMod(prop, mod, x, flipHorizontal)
return { x, y, z, frames }
}

Expand Down Expand Up @@ -277,8 +262,8 @@ export const itemInteraction = ({ mod, children }) => {
}

export const regionItemLayout = (prop, mod) => {
const [x, y, z] = propLocationFromObjectXY(prop, mod.x, mod.y)
const frames = propFramesFromMod(prop, mod)
const [x, y, z] = propLocationFromObjectXY(mod.x, mod.y)
const frames = propFramesFromMod(prop, mod, x)
return { x, y, z, frames }
}

Expand Down
143 changes: 125 additions & 18 deletions inspector/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,129 @@ export const frameFromText = (x, y, bytes, charset, pattern, fineXOffset, colors
return compositeLayers(layers)
}

const celLayerRenderer = {}
celLayerRenderer.default = (cel, colors, x, y) => {
if (cel.bitmap) {
return { canvas: canvasFromBitmap(cel.bitmap, colors), minX: x, minY: y - cel.height, maxX: x + cel.width, maxY: y }
} else {
return null
}
}

celLayerRenderer.text = (cel, colors, x, y) => {
const textColors = {...colors}
let pattern = cel.pattern
if (pattern == 0) {
// TODO: this is a bit of a hack; the C64 code would accept a pattern of 0q0101
// which would mean blue / wild / blue / wild. but canvasFromBitmap is not currently written
// in such a way that this would work. In practice, the pattern byte is always one of four values.
textColors.pattern = 15
textColors.wildcard = 6
pattern = 0x55
}
return frameFromText(x, y, colors.bytes, colors.charset, pattern, cel.fineXOffset, textColors)
}

celLayerRenderer.trap = (cel, colors, x, y) => {
const xOrigin = colors.xOrigin ?? 0

cel.x1a = (cel.raw.x1a + ((xOrigin + x) * 4)) % 256
cel.x1b = (cel.raw.x1b + ((xOrigin + x) * 4)) % 256
cel.x2a = (cel.raw.x2a + ((xOrigin + x) * 4)) % 256
cel.x2b = (cel.raw.x2b + ((xOrigin + x) * 4)) % 256
if (cel.x1b < cel.x1a) { cel.x1b += 256 }
if (cel.x2b < cel.x2a) { cel.x2b += 256 }
cel.xCorrection = Math.floor(Math.min(cel.x1a, cel.x2a) / 4)
cel.x1a -= cel.xCorrection * 4
cel.x1b -= cel.xCorrection * 4
cel.x2a -= cel.xCorrection * 4
cel.x2b -= cel.xCorrection * 4

// trapezoid-drawing algorithm:
// draw_line: draws a line from x1a,y1 to x1b, y1
// handles border drawing (last/first line, edges)
// decreases vcount, then jumps to cycle1 if there
// are more lines
// cycle1: run bresenham, determine if x1a (left edge) needs to be incremented
// or decremented (self-modifying code! the instruction in inc_dec1 is
// written at trap.m:52)
// has logic to jump back to cycle1 if we have a sharp enough angle that
// we need to move more than one pixel horizontally
// cycle2: same thing, but for x2a (right edge)
// at the end, increments y1 and jumps back to the top of draw_line
cel.width = Math.floor(Math.max(cel.x1a, cel.x1b, cel.x2a, cel.x2b) / 4) + 1
// trap.m:32 - delta_y and vcount are calculated by subtracting y2 - y1.
// mix.m:253: y2 is calculated as cel_y + cel_height
// mix.m:261: y1 is calculated as cel_y + 1
// So for a one-pixel tall trapezoid, deltay is 0, because y1 == y2.
// vcount is decremented until it reaches -1, compensating for the off-by-one.
const deltay = cel.height - 1
cel.bitmap = emptyBitmap(cel.width, cel.height)
const dxa = Math.abs(cel.x1a - cel.x2a)
const dxb = Math.abs(cel.x1b - cel.x2b)
const countMaxA = Math.max(dxa, deltay)
const countMaxB = Math.max(dxb, deltay)
const inca = cel.x1a < cel.x2a ? 1 : -1
const incb = cel.x1b < cel.x2b ? 1 : -1
let x1aLo = Math.floor(countMaxA / 2)
let y1aLo = x1aLo
let x1bLo = Math.floor(countMaxB / 2)
let y1bLo = x1bLo
let xa = cel.x1a
let xb = cel.x1b
for (let y = 0; y < cel.height; y ++) {
const line = cel.bitmap[y]
if (cel.border && (y == 0 || y == (cel.height - 1))) {
// top and bottom border line
horizontalLine(cel.bitmap, xa, xb, y, 0xaa, true)
} else {
if (cel.texture) {
const texLine = cel.texture[y % cel.texture.length]
for (let x = xa; x <= xb; x ++) {
line[x] = texLine[x % texLine.length]
}
} else {
horizontalLine(cel.bitmap, xa, xb, y, cel.pattern, cel.border)
}
}

if (cel.border) {
line[xa] = 2
line[xb] = 2
}

// cycle1: move xa
do {
x1aLo += dxa
if (x1aLo >= countMaxA) {
x1aLo -= countMaxA
xa += inca
}
y1aLo += deltay
} while (y1aLo < countMaxA)
y1aLo -= countMaxA

// cycle2: move xb
do {
x1bLo += dxb
if (x1bLo >= countMaxB) {
x1bLo -= countMaxB
xb += incb
}
y1bLo += deltay
} while (y1bLo < countMaxA)
y1bLo -= countMaxA
}
const celColors = { ...colors }
if (cel.texture) {
// dline.m:132: ; convert wild color to blue
// you can't have a trapezoid with a texture _and_ a pattern
celColors.pattern = 15
}
const canvas = canvasFromBitmap(cel.bitmap, celColors)
return { canvas, minX: cel.xCorrection - xOrigin, minY: y - cel.height, maxX: cel.xCorrection - xOrigin + cel.width, maxY: y }
}

// We try to consistently model Habitat's coordinate space in our rendering code as y=0 for the bottom, with increasing y meaning going up.
// However, the graphics code converts this internally to a coordinate space where increasing y means going down, and many internal
// coordinates (cel offsets, etc.) assume this.
Expand All @@ -267,26 +390,10 @@ export const frameFromCels = (cels, { colors: celColors, paintOrder, firstCelOri
yCorrect = cel.yOffset - cel.height
firstCelOrigin = false
}
const x = cel.xOffset + xRel + (cel.xCorrection ?? 0)
const x = cel.xOffset + xRel
const y = cel.yOffset + yRel
const colors = (Array.isArray(celColors) ? celColors[icel] : celColors) ?? {}
if (cel.bitmap) {
layers.push({ canvas: canvasFromBitmap(cel.bitmap, { ...colors, ...(cel.colorOverrides ?? {}) }), minX: x, minY: y - cel.height, maxX: x + cel.width, maxY: y })
} else if (cel.type == "text" && colors.bytes && colors.charset) {
const textColors = {...colors}
let pattern = cel.pattern
if (pattern == 0) {
// TODO: this is a bit of a hack; the C64 code would accept a pattern of 0q0101
// which would mean blue / wild / blue / wild. but canvasFromBitmap is not currently written
// in such a way that this would work. In practice, the pattern byte is always one of four values.
textColors.pattern = 15
textColors.wildcard = 6
pattern = 0x55
}
layers.push(frameFromText(x, y, colors.bytes, colors.charset, pattern, cel.fineXOffset, textColors))
} else {
layers.push(null)
}
layers.push((celLayerRenderer[cel.type] ?? celLayerRenderer.default)(cel, colors, x, y))
xRel += cel.xRel
yRel += cel.yRel
} else {
Expand Down

0 comments on commit 57722d5

Please sign in to comment.