Skip to content

Commit 6087475

Browse files
committed
Add workaround for alpha information getting lost on low opacity in TGA textures
Add Alpha Mask layer blend mode Change Split RGB channels into layer to support alpha channel, closes #3302 Add Split Alpha Channel into Layer option
1 parent 7a27b24 commit 6087475

File tree

7 files changed

+148
-8
lines changed

7 files changed

+148
-8
lines changed

js/interface/menu_bar.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,7 @@ export const MenuBar = {
353353
new MenuSeparator('filters'),
354354
'limit_to_palette',
355355
'split_rgb_into_layers',
356+
'split_alpha_into_layer',
356357
'clear_unused_texture_space',
357358
new MenuSeparator('transform'),
358359
'flip_texture_x',

js/texturing/edit_image.js

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { decodeTga } from "@lunapaint/tga-codec";
12
import { colorDistance } from "../util/util";
3+
import { fs } from "../native_apis";
24

35
BARS.defineActions(function() {
46

@@ -689,29 +691,45 @@ BARS.defineActions(function() {
689691
icon: 'stacked_bar_chart',
690692
category: 'textures',
691693
condition: {modes: ['paint'], selected: {texture: true}},
692-
click() {
694+
async click() {
693695
let texture = Texture.getDefault();
694696
let original_data = texture.ctx.getImageData(0, 0, texture.canvas.width, texture.canvas.height);
697+
if (texture.file_format == 'tga' && isApp) {
698+
699+
let data = fs.readFileSync(texture.path);
700+
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
701+
let result = await decodeTga(data);
702+
original_data.data.set(result.image.data);
703+
}
695704

696705
Undo.initEdit({textures: [texture], bitmap: true});
697706

698707
texture.layers_enabled = true;
699708
texture.layers.empty();
700709
let i = 0;
701-
for (let color of ['red', 'green', 'blue']) {
710+
for (let color of ['red', 'green', 'blue', 'alpha']) {
702711
let data_copy = new ImageData(original_data.data.slice(), original_data.width, original_data.height);
703712
for (let j = 0; j < data_copy.data.length; j += 4) {
704713
if (i != 0) data_copy.data[j+0] = 0;
705714
if (i != 1) data_copy.data[j+1] = 0;
706715
if (i != 2) data_copy.data[j+2] = 0;
716+
if (i == 3) {
717+
data_copy.data[j+0] = data_copy.data[j+1] = data_copy.data[j+2] = data_copy.data[j+3];
718+
}
719+
data_copy.data[j+3] = 255;
707720
}
708721
let layer = new TextureLayer({
709722
name: color,
710723
blend_mode: 'add'
711724
}, texture);
712725
layer.setSize(original_data.width, original_data.height);
713726
layer.ctx.putImageData(data_copy, 0, 0);
714-
texture.layers.unshift(layer);
727+
if (color == 'alpha') {
728+
texture.layers.push(layer);
729+
layer.blend_mode = 'alpha_mask'
730+
} else {
731+
texture.layers.unshift(layer);
732+
}
715733
if (color == 'red') {
716734
layer.select();
717735
}
@@ -723,6 +741,54 @@ BARS.defineActions(function() {
723741
BARS.updateConditions();
724742
}
725743
})
744+
new Action('split_alpha_into_layer', {
745+
icon: 'tab_inactive',
746+
category: 'textures',
747+
condition: {modes: ['paint'], selected: {texture: true}},
748+
async click() {
749+
let texture = Texture.getDefault();
750+
let original_data = texture.ctx.getImageData(0, 0, texture.canvas.width, texture.canvas.height);
751+
if (texture.file_format == 'tga' && isApp) {
752+
753+
let data = fs.readFileSync(texture.path);
754+
if (data instanceof ArrayBuffer) data = new Uint8Array(data);
755+
let result = await decodeTga(data);
756+
original_data.data.set(result.image.data);
757+
}
758+
759+
Undo.initEdit({textures: [texture], bitmap: true});
760+
761+
texture.layers_enabled = true;
762+
texture.layers.empty();
763+
764+
// Color
765+
let data_copy = new ImageData(original_data.data.slice(), original_data.width, original_data.height);
766+
for (let j = 0; j < data_copy.data.length; j += 4) {
767+
data_copy.data[j+3] = 255;
768+
}
769+
let layer = new TextureLayer({name: 'color'}, texture);
770+
layer.setSize(original_data.width, original_data.height);
771+
layer.ctx.putImageData(data_copy, 0, 0);
772+
texture.layers.push(layer);
773+
layer.select();
774+
775+
// Alpha
776+
let data_alpha = new ImageData(original_data.data.slice(), original_data.width, original_data.height);
777+
for (let j = 0; j < data_alpha.data.length; j += 4) {
778+
data_alpha.data[j+0] = data_alpha.data[j+1] = data_alpha.data[j+2] = data_alpha.data[j+3];
779+
data_alpha.data[j+3] = 255;
780+
}
781+
let alpha_layer = new TextureLayer({name: 'alpha', blend_mode: 'alpha_mask'}, texture);
782+
alpha_layer.setSize(original_data.width, original_data.height);
783+
alpha_layer.ctx.putImageData(data_alpha, 0, 0);
784+
texture.layers.push(alpha_layer);
785+
786+
texture.updateLayerChanges(true);
787+
Undo.finishEdit('Split texture alpha into alpha mask layers');
788+
updateInterfacePanels();
789+
BARS.updateConditions();
790+
}
791+
})
726792
new Action('clear_unused_texture_space', {
727793
icon: 'cleaning_services',
728794
category: 'textures',

js/texturing/layers.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ new Property(TextureLayer, 'vector2', 'offset');
367367
new Property(TextureLayer, 'vector2', 'scale', {default: [1, 1]});
368368
new Property(TextureLayer, 'number', 'opacity', {default: 100});
369369
new Property(TextureLayer, 'boolean', 'visible', {default: true});
370-
new Property(TextureLayer, 'enum', 'blend_mode', {default: 'default', values: ['default', 'set_opacity', 'color', 'multiply', 'add', 'darken', 'lighten', 'screen', 'overlay', 'difference']});
370+
new Property(TextureLayer, 'enum', 'blend_mode', {default: 'default', values: ['default', 'set_opacity', 'color', 'multiply', 'add', 'darken', 'lighten', 'screen', 'overlay', 'difference', 'alpha_mask']});
371371
new Property(TextureLayer, 'boolean', 'in_limbo', {default: false});
372372

373373
Object.defineProperty(TextureLayer, 'all', {

js/texturing/textures.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,6 +1773,18 @@ export class Texture {
17731773
this.canvas.height = this.height;
17741774
for (let layer of this.layers) {
17751775
if (layer.visible == false || layer.opacity == 0) continue;
1776+
if (layer.blend_mode == 'alpha_mask') {
1777+
let mask = layer.ctx.getImageData(0, 0, layer.canvas.width, layer.canvas.height);
1778+
Painter.scanCanvas(this.ctx, 0, 0, this.canvas.width, this.canvas.height, (px, py, color) => {
1779+
let mask_coords = [ px - layer.offset[0], py - layer.offset[1] ];
1780+
if (mask_coords[0] < 0 || mask_coords[0] >= layer.canvas.width) return;
1781+
if (mask_coords[1] < 0 || mask_coords[1] >= layer.canvas.height) return;
1782+
let value = mask.data[(mask_coords[1] * layer.canvas.width + mask_coords[0]) * 4];
1783+
color[3] *= value / 255;
1784+
return color;
1785+
})
1786+
continue;
1787+
}
17761788
this.ctx.filter = `opacity(${layer.opacity / 100})`;
17771789
this.ctx.globalCompositeOperation = Painter.getBlendModeCompositeOperation(layer.blend_mode);
17781790
this.ctx.imageSmoothingEnabled = false;
@@ -1861,6 +1873,16 @@ export class Texture {
18611873
extensions: ['tga'],
18621874
async encode(texture) {
18631875
let image_data = texture.ctx.getImageData(0, 0, texture.canvas.width, texture.canvas.height);
1876+
1877+
let supported_blend_modes = ['add', 'alpha_mask', 'default'];
1878+
if (
1879+
texture.layers_enabled &&
1880+
texture.layers.some(l => l.blend_mode == 'alpha_mask') &&
1881+
texture.layers.allAre(l => supported_blend_modes.includes(l.blend_mode) && l.scale.allEqual(1))
1882+
) {
1883+
image_data = getTextureDataWithAccurateAlpha(texture);
1884+
}
1885+
18641886
let result = await encodeTga({
18651887
data: image_data.data,
18661888
width: texture.canvas.width,
@@ -2096,6 +2118,7 @@ export class Texture {
20962118
new MenuSeparator('filters'),
20972119
'limit_to_palette',
20982120
'split_rgb_into_layers',
2121+
'split_alpha_into_layer',
20992122
'clear_unused_texture_space',
21002123
new MenuSeparator('transform'),
21012124
'flip_texture_x',
@@ -2310,6 +2333,49 @@ export function getTexturesById(id) {
23102333
return $.grep(Texture.all, function(e) {return e.id == id});
23112334
}
23122335

2336+
/**
2337+
* Function to merge layers while preserving accurate alpha channel information. Does not support some blend modes, or layer scaling
2338+
*/
2339+
function getTextureDataWithAccurateAlpha(texture) {
2340+
let data = texture.ctx.createImageData(texture.canvas.width, texture.canvas.height);
2341+
for (let layer of texture.layers) {
2342+
if (layer.visible == false || layer.opacity == 0) continue;
2343+
2344+
let layer_data = layer.ctx.getImageData(0, 0, layer.canvas.width, layer.canvas.height);
2345+
2346+
for (let x = 0; x < texture.canvas.width; x++) {
2347+
for (let y = 0; y < texture.canvas.height; y++) {
2348+
let index = data.getIndex(x, y);
2349+
let layer_index = layer_data.getIndex(x - layer.offset[0], y - layer.offset[1]);
2350+
let alpha = (layer_data.data[layer_index+3] / 255) * (layer.opacity/100);
2351+
2352+
switch (layer.blend_mode) {
2353+
case 'default': {
2354+
data.data[index+0] = Math.lerp(data.data[index+0], layer_data.data[layer_index+0], alpha);
2355+
data.data[index+1] = Math.lerp(data.data[index+1], layer_data.data[layer_index+1], alpha);
2356+
data.data[index+2] = Math.lerp(data.data[index+2], layer_data.data[layer_index+2], alpha);
2357+
data.data[index+3] = Math.lerp(data.data[index+3], 255, alpha);
2358+
break;
2359+
}
2360+
case 'add': {
2361+
data.data[index+0] += layer_data.data[layer_index+0] * alpha;
2362+
data.data[index+1] += layer_data.data[layer_index+1] * alpha;
2363+
data.data[index+2] += layer_data.data[layer_index+2] * alpha;
2364+
data.data[index+3] += layer_data.data[layer_index+3] * alpha;
2365+
break;
2366+
}
2367+
case 'alpha_mask': {
2368+
let value = layer_data.data[layer_index];
2369+
data.data[index+3] *= (value / 255) * alpha;
2370+
break;
2371+
}
2372+
}
2373+
}
2374+
}
2375+
}
2376+
return data;
2377+
}
2378+
23132379
SharedActions.add('delete', {
23142380
condition: () => Prop.active_panel == 'textures' && Texture.selected,
23152381
run() {

js/util/util.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ Object.defineProperty($.Event.prototype, 'ctrlOrCmd', {
104104
return this.ctrlKey || this.metaKey;
105105
}
106106
})
107+
ImageData.prototype.getIndex = function(x, y) {
108+
if (x < 0 || y < 0 || x >= this.width || y >= this.height) return null;
109+
return (x + y * this.height) * 4;
110+
}
107111

108112
export function convertTouchEvent(event) {
109113
if (event && event.changedTouches && event.changedTouches.length && event.offsetX == undefined) {

lang/en.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,7 @@
14081408
"action.blend_mode.overlay": "Overlay",
14091409
"action.blend_mode.hard_light": "Hard Light",
14101410
"action.blend_mode.difference": "Difference",
1411+
"action.blend_mode.alpha_mask": "Alpha Mask",
14111412
"action.expand_texture_selection": "Expand Texture Selection",
14121413
"action.expand_texture_selection.desc": "Expand or shrink the texture selection",
14131414

@@ -1898,8 +1899,10 @@
18981899
"action.adjust_curves.desc": "Adjust the brightness curves of the selected texture",
18991900
"action.limit_to_palette": "Limit to Palette",
19001901
"action.limit_to_palette.desc": "Limits the colors of the texture to those in the currently loaded palette",
1901-
"action.split_rgb_into_layers": "Split RGB Channels into Layers",
1902-
"action.split_rgb_into_layers.desc": "Split the texture into additive layers, one for each RGB channel",
1902+
"action.split_rgb_into_layers": "Split RGBA Channels into Layers",
1903+
"action.split_rgb_into_layers.desc": "Split the texture into additive layers, one for each RGBA channel",
1904+
"action.split_alpha_into_layer": "Split Alpha Channel into Layer",
1905+
"action.split_alpha_into_layer.desc": "Split the alpha channel of the texture into an alpha mask channel",
19031906
"action.clear_unused_texture_space": "Clear Unused Texture Space",
19041907
"action.clear_unused_texture_space.desc": "Clear parts of the texture that are not UV-mapped to any elements",
19051908
"action.flip_texture_x": "Flip Horizontally",

types/custom/texture_layers.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ interface TextureLayerData {
77
scale?: ArrayVector2
88
opacity?: number
99
visible?: boolean
10-
blend_mode?: 'default' | 'set_opacity' | 'color' | 'multiply' | 'add' | 'screen' | 'difference'
10+
blend_mode?: 'default' | 'set_opacity' | 'color' | 'multiply' | 'add' | 'darken' | 'lighten' | 'screen' | 'overlay' | 'difference' | 'alpha_mask'
1111
image_data?: ImageData
1212
data_url?: string
1313
}
@@ -35,7 +35,7 @@ declare class TextureLayer {
3535
scale: ArrayVector2
3636
opacity: number
3737
visible: boolean
38-
blend_mode: 'default' | 'set_opacity' | 'color' | 'multiply' | 'add' | 'screen' | 'difference'
38+
blend_mode: 'default' | 'set_opacity' | 'color' | 'multiply' | 'add' | 'darken' | 'lighten' | 'screen' | 'overlay' | 'difference' | 'alpha_mask'
3939

4040
extend(data: TextureLayerData): void
4141
/**

0 commit comments

Comments
 (0)