From 57722d5205ab2304b4ef29086e8d29bf7aa9d00d Mon Sep 17 00:00:00 2001 From: Jeremy Penner Date: Fri, 2 Feb 2024 00:55:13 -0500 Subject: [PATCH] Move trapezoid rendering out of the decoder, remove byte-wrap hack --- inspector/codec.js | 91 +--------------------------- inspector/region.js | 51 ++++++---------- inspector/render.js | 143 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 144 insertions(+), 141 deletions(-) diff --git a/inspector/codec.js b/inspector/codec.js index 0ce2807b..bda70df5 100644 --- a/inspector/codec.js +++ b/inspector/codec.js @@ -222,10 +222,8 @@ 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), @@ -233,93 +231,6 @@ celDecoder.trap = (data, cel) => { 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) => { diff --git a/inspector/region.js b/inspector/region.js index be9d2db9..2851a1ac 100644 --- a/inspector/region.js +++ b/inspector/region.js @@ -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) { @@ -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 @@ -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 @@ -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) => { @@ -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) { @@ -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 } } @@ -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 } } diff --git a/inspector/render.js b/inspector/render.js index 01ccebdc..bad82307 100644 --- a/inspector/render.js +++ b/inspector/render.js @@ -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. @@ -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 {