Displacement map prototype (#26709)

Requires https://github.com/space-wizards/RobustToolbox/pull/5023

This uses the new engine features (above) to add a displacement map shader. This allows deforming a sprite based on another sprite.

Primary use case is automatically adapting human clothing sprites to different species, something we want to make species like Vox a reality.

A basic example of wiring this up with Vox has been added. The system is however incredibly simple and **will** need more work by a content developer to select and toggle displacement maps when appropriate. I am leaving that to somebody else. For example right now the displacement map is applied even if a species already has custom-fit sprites for a piece of clothing, such as the grey jumpsuit for Vox.

Basic Aseprite plugins to help with authoring displacement maps have also been made.

(cherry picked from commit 2f7d0dedbd)
This commit is contained in:
Pieter-Jan Briers
2024-05-03 00:28:42 +02:00
committed by null
parent e0db333d31
commit b2294a8cdc
9 changed files with 303 additions and 1 deletions
@@ -0,0 +1,78 @@
local sprite = app.editor.sprite
local cel = app.cel
if sprite.selection.isEmpty then
print("You need to select something sorry")
return
end
local diag = Dialog{
title = "Flip Displacement Map"
}
diag:check{
id = "horizontal",
label = "flip horizontal?"
}
diag:check{
id = "vertical",
label = "flip vertical?"
}
diag:button{
text = "ok",
focus = true,
onclick = function(ev)
local horizontal = diag.data["horizontal"]
local vertical = diag.data["vertical"]
local selection = sprite.selection
local image = cel.image:clone()
for x = 0, selection.bounds.width do
for y = 0, selection.bounds.height do
local xSel = x + selection.origin.x
local ySel = y + selection.origin.y
local xImg = xSel - cel.position.x
local yImg = ySel - cel.position.y
if xImg < 0 or xImg >= image.width or yImg < 0 or yImg >= image.height then
goto continue
end
local imgValue = image:getPixel(xImg, yImg)
local color = Color(imgValue)
if horizontal then
color.red = 128 + -(color.red - 128)
end
if vertical then
color.green = 128 + -(color.green - 128)
end
image:drawPixel(
xImg,
yImg,
app.pixelColor.rgba(color.red, color.green, color.blue, color.alpha))
::continue::
end
end
cel.image = image
diag:close()
end
}
diag:button{
text = "cancel",
onclick = function(ev)
diag:close()
end
}
diag:show()
@@ -0,0 +1,135 @@
-- Displacement Map Visualizer
--
-- This script will create a little preview window that will test a displacement map.
--
-- TODO: Handling of sizes != 127 doesn't work properly and rounds differently from the real shader. Ah well.
local scale = 4
-- This script requires UI
if not app.isUIAvailable then
return
end
local getOffsetPixel = function(x, y, image, rect)
local posX = x - rect.x
local posY = y - rect.y
if posX < 0 or posX >= image.width or posY < 0 or posY >= image.height then
return image.spec.transparentColor
end
return image:getPixel(posX, posY)
end
local pixelValueToColor = function(sprite, value)
return Color(value)
end
local applyDisplacementMap = function(width, height, size, displacement, displacementRect, target, targetRect)
-- print(Color(displacement:getPixel(17, 15)).red)
local image = target:clone()
image:resize(width, height)
image:clear()
for x = 0, width - 1 do
for y = 0, height - 1 do
local value = getOffsetPixel(x, y, displacement, displacementRect)
local color = pixelValueToColor(sprite, value)
if color.alpha ~= 0 then
local offset_x = (color.red - 128) / 127 * size
local offset_y = (color.green - 128) / 127 * size
local colorValue = getOffsetPixel(x + offset_x, y + offset_y, target, targetRect)
image:drawPixel(x, y, colorValue)
end
end
end
return image
end
local dialog = nil
local sprite = app.editor.sprite
local spriteChanged = sprite.events:on("change",
function(ev)
dialog:repaint()
end)
local layers = {}
for i,layer in ipairs(sprite.layers) do
table.insert(layers, 1, layer.name)
end
local findLayer = function(sprite, name)
for i, layer in ipairs(sprite.layers) do
if layer.name == name then
return layer
end
end
return nil
end
dialog = Dialog{
title = "Displacement map preview",
onclose = function(ev)
sprite.events:off(spriteChanged)
end}
dialog:canvas{
id = "canvas",
width = sprite.width * scale,
height = sprite.height * scale,
onpaint = function(ev)
local context = ev.context
local layerDisplacement = findLayer(sprite, dialog.data["displacement-select"])
local layerTarget = findLayer(sprite, dialog.data["reference-select"])
-- print(layerDisplacement.name)
-- print(layerTarget.name)
local celDisplacement = layerDisplacement:cel(1)
local celTarget = layerTarget:cel(1)
local image = applyDisplacementMap(
sprite.width, sprite.height,
dialog.data["size"],
celDisplacement.image, celDisplacement.bounds,
celTarget.image, celTarget.bounds)
context:drawImage(image, 0, 0, image.width, image.height, 0, 0, image.width * scale, context.width, context.height)
end
}
dialog:combobox{
id = "displacement-select",
label = "displacement layer",
options = layers,
onchange = function(ev)
dialog:repaint()
end
}
dialog:combobox{
id = "reference-select",
label = "reference layer",
options = layers,
onchange = function(ev)
dialog:repaint()
end
}
dialog:slider{
id = "size",
label = "displacement size",
min = 1,
max = 127,
value = 127,
onchange = function(ev)
dialog:repaint()
end
}
dialog:show{wait = false}