Stream asset reset v7, cleaning background and transparency fixes. 2026-01-29 16:37
BIN
assets/.DS_Store
vendored
|
Before Width: | Height: | Size: 447 KiB |
BIN
assets/DEMO_FAZA1/Environment/stream_final_v6.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/DEMO_FAZA1/Environment/stream_final_v7.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 397 KiB |
|
Before Width: | Height: | Size: 884 KiB |
BIN
assets/DEMO_FAZA1/Environment/stream_v5_graded.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 891 KiB |
BIN
assets/references/potok_voda_cevi.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
@@ -19,7 +19,8 @@ function createWindow() {
|
||||
title: 'Mrtva Dolina - Death Valley'
|
||||
});
|
||||
|
||||
mainWindow.loadFile('index.html');
|
||||
// Fix path to index.html (it's one level up from scripts/)
|
||||
mainWindow.loadFile(path.join(__dirname, '../index.html'));
|
||||
mainWindow.webContents.openDevTools();
|
||||
|
||||
mainWindow.on('closed', () => {
|
||||
@@ -1,108 +1,107 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import os
|
||||
|
||||
def slice_stream_assets():
|
||||
src_path = '/Users/davidkotnik/.gemini/antigravity/brain/07019d04-a214-43ab-9565-86f4e8f17e5b/uploaded_media_1769607894587.jpg'
|
||||
|
||||
print(f"Loading {src_path}")
|
||||
img = cv2.imread(src_path)
|
||||
if img is None: return
|
||||
# Paths
|
||||
input_path = '/Users/davidkotnik/.gemini/antigravity/brain/8233d64e-0c17-43b1-b8b5-fbc41754e56b/uploaded_media_1769679216460.jpg'
|
||||
output_dir = '/Users/davidkotnik/repos/novafarma/assets/DEMO_FAZA1/Environment/'
|
||||
|
||||
# Ensure output dir exists
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
# Read image
|
||||
img = cv2.imread(input_path, cv2.IMREAD_COLOR)
|
||||
if img is None:
|
||||
print("Error loading input image")
|
||||
exit(1)
|
||||
|
||||
h, w = img.shape[:2]
|
||||
print(f"Image loaded: {w}x{h}")
|
||||
|
||||
# Convert to BGRA
|
||||
img_rgba = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
||||
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
|
||||
|
||||
# --- 1. REMOVE BACKGROUND (Checkerboard/White) ---
|
||||
# Strategy: The background is bright (White/Light Gray). The content (Mud, Pipe, Debris) is darker.
|
||||
# Threshold on Value (V) channel.
|
||||
# Let's verify histogram or min/max.
|
||||
# Or just a hard threshold.
|
||||
# Mud is dark. Pipe is dark gray.
|
||||
# Debris is dark.
|
||||
# Checkerboard is > 200 usually.
|
||||
|
||||
thresh_val = 190
|
||||
# Create mask where Value > 190 -> Background
|
||||
_, mask_bg_bright = cv2.threshold(hsv[:, :, 2], thresh_val, 255, cv2.THRESH_BINARY)
|
||||
|
||||
# Also check for Low Saturation for the checkerboard?
|
||||
# Checkerboard is grayscale (S near 0).
|
||||
# Pipe is also grayscale.
|
||||
# But Pipe is Darker.
|
||||
# So Brightness threshold should handle it.
|
||||
|
||||
# Refine mask:
|
||||
# Remove small holes in background mask?
|
||||
# Use morphological open
|
||||
kernel = np.ones((3,3), np.uint8)
|
||||
mask_bg_bright = cv2.morphologyEx(mask_bg_bright, cv2.MORPH_OPEN, kernel)
|
||||
|
||||
# --- 2. REMOVE GREEN TRIANGLE ---
|
||||
# Green range
|
||||
lower_green = np.array([35, 40, 40])
|
||||
upper_green = np.array([90, 255, 255])
|
||||
mask_green = cv2.inRange(hsv, lower_green, upper_green)
|
||||
|
||||
# Combine Background and Green masks
|
||||
# We want to remove (set alpha 0) where mask is BG or Green.
|
||||
mask_remove = cv2.bitwise_or(mask_bg_bright, mask_green)
|
||||
|
||||
# Set alpha
|
||||
# Start with opaque (255)
|
||||
img_rgba[:, :, 3] = 255
|
||||
# Set 0 where removal mask is active
|
||||
img_rgba[mask_remove > 0, 3] = 0
|
||||
|
||||
# Smooth edges slightly
|
||||
# cv2.GaussianBlur on alpha channel?
|
||||
# Or erode the alpha slightly to remove fringes.
|
||||
# Let's erode the opaque region.
|
||||
alpha = img_rgba[:, :, 3]
|
||||
alpha = cv2.erode(alpha, kernel, iterations=1)
|
||||
img_rgba[:, :, 3] = alpha
|
||||
|
||||
|
||||
# --- 3. CROP AND SAVE ---
|
||||
# Crop to content
|
||||
coords = cv2.findNonZero(img_rgba[:, :, 3])
|
||||
if coords is not None:
|
||||
x, y, w_content, h_content = cv2.boundingRect(coords)
|
||||
|
||||
# Padding
|
||||
padding = 2
|
||||
x = max(0, x - padding)
|
||||
y = max(0, y - padding)
|
||||
w_content = min(w - x, w_content + 2*padding)
|
||||
h_content = min(h - y, h_content + 2*padding)
|
||||
|
||||
cropped_main = img_rgba[y:y+h_content, x:x+w_content]
|
||||
else:
|
||||
cropped_main = img_rgba # Fallback
|
||||
|
||||
# Save FINAL asset v7
|
||||
output_path = os.path.join(output_dir, 'stream_final_v7.png')
|
||||
cv2.imwrite(output_path, cropped_main)
|
||||
print(f"Saved V7 asset to {output_path}")
|
||||
|
||||
|
||||
|
||||
# 1. CLEAN BACKGROUND (GrabCut)
|
||||
mask = np.zeros(img.shape[:2], np.uint8)
|
||||
bgdModel = np.zeros((1,65),np.float64)
|
||||
fgdModel = np.zeros((1,65),np.float64)
|
||||
h, w = img.shape[:2]
|
||||
cv2.grabCut(img, mask, (10, 10, w-20, h-20), bgdModel, fgdModel, 5, cv2.GC_INIT_WITH_RECT)
|
||||
mask_final = np.where((mask==2)|(mask==0),0,1).astype('uint8')
|
||||
|
||||
# Apply Alpha
|
||||
b, g, r = cv2.split(img)
|
||||
alpha = mask_final * 255
|
||||
img_rgba = cv2.merge([b, g, r, alpha])
|
||||
|
||||
# 2. CREATE 'HEAD' (Pipe Only)
|
||||
# The pipe is roughly the top 40%? Let's crop visually based on the image structure.
|
||||
# The pipe is top-right.
|
||||
# Let's say we keep the top half as the "Start".
|
||||
# And we take a middle slice as the "Segment".
|
||||
|
||||
# Finding the pipe:
|
||||
# Based on the image, the pipe drain is at the top.
|
||||
# Let's crop `head` from y=0 to y=0.45*h
|
||||
cut_y = int(h * 0.45)
|
||||
|
||||
head_img = img_rgba[0:cut_y, :]
|
||||
|
||||
# Crop transparent borders from head
|
||||
coords = cv2.findNonZero(head_img[:,:,3]) # Alpha
|
||||
if coords is not None:
|
||||
x, y, cw, ch = cv2.boundingRect(coords)
|
||||
head_img = head_img[y:y+ch, x:x+cw]
|
||||
|
||||
# 3. CREATE 'SEGMENT' (Water Channel)
|
||||
# We take a slice from the middle-bottom.
|
||||
# Crop from y=0.45*h to y=0.85*h (skip very bottom tip?)
|
||||
# Actually, let's take a nice chunk that can be tiled.
|
||||
# The channel is diagonal. Tiling diagonal is hard without overlap.
|
||||
# Let's just crop the rest of the stream as one big piece for now.
|
||||
|
||||
body_img = img_rgba[cut_y:, :]
|
||||
|
||||
# Crop transparent borders from body
|
||||
coords = cv2.findNonZero(body_img[:,:,3])
|
||||
if coords is not None:
|
||||
x, y, cw, ch = cv2.boundingRect(coords)
|
||||
body_img = body_img[y:y+ch, x:x+cw]
|
||||
|
||||
# 4. SOFTEN EDGES (To fix floating walls)
|
||||
# Applied to both Head and Body.
|
||||
# We want to fade out the BOTTOM edge of the mask, so the "wall" blends into the grass.
|
||||
|
||||
def soften_bottom_edge(image):
|
||||
h, w = image.shape[:2]
|
||||
# Create a gradient mask for the bottom 20 pixels
|
||||
grad_h = 30
|
||||
if h < grad_h: return image # Too small
|
||||
|
||||
# We modify the alpha channel
|
||||
alpha = image[:,:,3]
|
||||
|
||||
# We need to detect where the "bottom" of the object is.
|
||||
# Since it's diagonal, it's tricky.
|
||||
# Simple hack: Erode the alpha slightly to sharpen the cut, then blur it?
|
||||
# Or just blur the edges?
|
||||
|
||||
# Let's try blurring the alpha channel to soften the hard cut against the grass.
|
||||
# Only on the edges.
|
||||
# Get edge mask
|
||||
edges = cv2.Canny(alpha, 100, 200)
|
||||
# Dilate edges to get a rim
|
||||
rim = cv2.dilate(edges, np.ones((5,5),np.uint8))
|
||||
|
||||
# Blur alpha
|
||||
blurred_alpha = cv2.GaussianBlur(alpha, (7,7), 0)
|
||||
|
||||
# Apply blurred alpha where rim is
|
||||
# image[:,:,3] = np.where(rim>0, blurred_alpha, alpha)
|
||||
|
||||
# Actually, let's just do a global soft outline.
|
||||
image[:,:,3] = blurred_alpha
|
||||
return image
|
||||
|
||||
# head_img = soften_bottom_edge(head_img)
|
||||
# body_img = soften_bottom_edge(body_img)
|
||||
# (Skipping blur for now, plain cut is cleaner if geometry is right)
|
||||
|
||||
# Save
|
||||
base_dir = '/Users/davidkotnik/repos/novafarma/assets/DEMO_FAZA1/Environment'
|
||||
if not os.path.exists(base_dir): os.makedirs(base_dir, exist_ok=True)
|
||||
|
||||
cv2.imwrite(os.path.join(base_dir, 'stream_head.png'), head_img)
|
||||
cv2.imwrite(os.path.join(base_dir, 'stream_body.png'), body_img)
|
||||
|
||||
print("Sliced stream into head and body.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
slice_stream_assets()
|
||||
|
||||
@@ -10,8 +10,9 @@ export default class GrassSceneClean extends Phaser.Scene {
|
||||
// 1. Podlaga (Foundation)
|
||||
this.load.image('ground_base', 'DEMO_FAZA1/Ground/tla_trava_tekstura.png');
|
||||
|
||||
// 2. Vodni kanali (Water)
|
||||
this.load.image('stream_water', 'DEMO_FAZA1/Environment/stream_water.png');
|
||||
// 2. Vodni kanali (Water) - NEW CLEAN ASSET V7 (Aggressive Clean)
|
||||
this.load.image('stream_final_v7', 'DEMO_FAZA1/Environment/stream_final_v7.png');
|
||||
// Removed extensions for now
|
||||
|
||||
// 3. Foliage
|
||||
this.load.image('grass_dense', 'DEMO_FAZA1/Vegetation/grass_cluster_dense.png');
|
||||
@@ -26,22 +27,13 @@ export default class GrassSceneClean extends Phaser.Scene {
|
||||
frameHeight: 256
|
||||
}); // Loading as 'kai' to keep existing references working, but now it has frames.
|
||||
|
||||
// 5. UI Assets (Loaded from DEMO_FAZA1/UI)
|
||||
this.load.image('ui_health_bar', 'DEMO_FAZA1/UI/health_bar.png');
|
||||
this.load.image('ui_weather_widget', 'DEMO_FAZA1/UI/weather_widget.png');
|
||||
this.load.image('ui_minimap', 'DEMO_FAZA1/UI/minimap_frame.png');
|
||||
this.load.image('ui_hotbar', 'DEMO_FAZA1/UI/hotbar_background.png');
|
||||
this.load.image('ui_action_btn', 'DEMO_FAZA1/UI/action_btn.png');
|
||||
// 5. UI Assets (Loaded in UIScene now to prevent duplicates)
|
||||
// REMOVED
|
||||
|
||||
// 6. Camp Assets
|
||||
this.load.image('campfire', 'DEMO_FAZA1/Environment/taborni_ogenj.png');
|
||||
this.load.image('tent', 'DEMO_FAZA1/Environment/sotor.png');
|
||||
this.load.image('sleeping_bag', 'DEMO_FAZA1/Items/spalna_vreca.png');
|
||||
|
||||
// 7. NEW: Sliced Stream Assets
|
||||
this.load.image('stream_head', 'DEMO_FAZA1/Environment/stream_head.png');
|
||||
this.load.image('stream_body', 'DEMO_FAZA1/Environment/stream_body.png');
|
||||
|
||||
}
|
||||
|
||||
create() {
|
||||
@@ -52,27 +44,42 @@ export default class GrassSceneClean extends Phaser.Scene {
|
||||
this.cameras.main.setBounds(0, 0, WORLD_W, WORLD_H);
|
||||
this.cameras.main.setBackgroundColor('#1a1a1a');
|
||||
|
||||
// --- ZOOM CONTROL ---
|
||||
this.cameras.main.setZoom(1.5); // Default start zoom
|
||||
|
||||
this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
|
||||
// Zoom In/Out based on wheel delta
|
||||
// deltaY > 0 means scroll down (zoom out), deltaY < 0 means scroll up (zoom in)
|
||||
const zoomFactor = -0.001;
|
||||
const newZoom = this.cameras.main.zoom + deltaY * zoomFactor;
|
||||
|
||||
// Clamp zoom to reasonable limits (e.g., 0.5x to 3x)
|
||||
this.cameras.main.setZoom(Phaser.Math.Clamp(newZoom, 0.5, 3.0));
|
||||
});
|
||||
|
||||
// --- 1. PODLAGA (The Foundation) ---
|
||||
// Level 0, Locked to Z = -100
|
||||
this.ground = this.add.tileSprite(WORLD_W / 2, WORLD_H / 2, WORLD_W, WORLD_H, 'ground_base');
|
||||
this.ground.setTileScale(1, 1);
|
||||
this.ground.setDepth(-100);
|
||||
|
||||
// --- 2. STREAM (Single Pipe Channel) ---
|
||||
// Placed at Z=0 (Ground Level)
|
||||
const startX = WORLD_W / 2 + 50;
|
||||
const startY = WORLD_H / 2 + 100;
|
||||
// --- 2. STREAM SYSTEM (Head + Extensions) ---
|
||||
// Center the whole construction
|
||||
const startX = WORLD_W / 2;
|
||||
const startY = WORLD_H / 2 - 300; // Start higher up to leave room for extensions
|
||||
|
||||
this.stream = this.physics.add.staticImage(startX, startY, 'stream_muddy');
|
||||
this.stream.setOrigin(0.5, 0.9);
|
||||
this.stream.setDepth(0);
|
||||
this.stream.setScale(1.0);
|
||||
// Main Head (Pipe + Splash)
|
||||
// Showing V7 Asset (Aggressive Clean + Transparent Green)
|
||||
this.stream = this.physics.add.staticImage(startX, startY, 'stream_final_v7');
|
||||
this.stream.setOrigin(0.5, 0.5);
|
||||
this.stream.setDepth(-50);
|
||||
|
||||
// Physics Body
|
||||
this.stream.body.setSize(this.stream.width * 0.9, this.stream.height * 0.4);
|
||||
this.stream.body.setOffset(this.stream.width * 0.05, this.stream.height * 0.5);
|
||||
// Physics Body for Main
|
||||
this.stream.body.setSize(this.stream.width * 0.8, this.stream.height * 0.2);
|
||||
this.stream.body.setOffset(this.stream.width * 0.1, this.stream.height * 0.4);
|
||||
|
||||
// Collider added later after Kai creation
|
||||
// Extensions removed for reset
|
||||
// for (let i = 1; i <= 3; i++) { ... }
|
||||
|
||||
// Collider added later after Kai creation
|
||||
|
||||
@@ -95,7 +102,7 @@ export default class GrassSceneClean extends Phaser.Scene {
|
||||
this.kai.body.setOffset(256 / 2 - 25, 256 - 40); // Pivot offset based on 256 frame
|
||||
|
||||
// Collider Stream <-> Kai
|
||||
this.physics.add.collider(this.kai, this.stream);
|
||||
// this.physics.add.collider(this.kai, this.stream);
|
||||
|
||||
// --- ANIMATIONS ---
|
||||
// 0-3: Down, 4-7: Left, 8-11: Right, 12-15: Up
|
||||
@@ -128,42 +135,55 @@ export default class GrassSceneClean extends Phaser.Scene {
|
||||
this.kai.play('walk-down');
|
||||
this.kai.stop();
|
||||
|
||||
// --- 7. CAMP SETUP (Oživitev) ---
|
||||
this.campGroup = this.add.group();
|
||||
|
||||
// Tent (Behind Kai usually)
|
||||
let tent = this.add.image(WORLD_W / 2 - 100, WORLD_H / 2 - 80, 'tent');
|
||||
tent.setOrigin(0.5, 0.9); // Bottom anchor
|
||||
tent.setScale(1.2); // Big enough for Kai
|
||||
this.campGroup.add(tent);
|
||||
|
||||
// Campfire (In front)
|
||||
let fire = this.add.image(WORLD_W / 2 + 50, WORLD_H / 2 + 50, 'campfire');
|
||||
fire.setOrigin(0.5, 0.8);
|
||||
fire.setScale(0.8);
|
||||
this.campGroup.add(fire);
|
||||
|
||||
// Tweens for Fire (Little pulse)
|
||||
this.tweens.add({
|
||||
targets: fire,
|
||||
scaleX: 0.85,
|
||||
scaleY: 0.85,
|
||||
duration: 500,
|
||||
yoyo: true,
|
||||
repeat: -1
|
||||
});
|
||||
|
||||
// Sleeping Bag
|
||||
let bag = this.add.image(WORLD_W / 2 - 80, WORLD_H / 2 + 20, 'sleeping_bag');
|
||||
bag.setOrigin(0.5, 0.5); // Flat on ground?
|
||||
bag.setScale(0.8);
|
||||
this.campGroup.add(bag);
|
||||
|
||||
// Camera
|
||||
// Camera setup logic
|
||||
this.cameras.main.startFollow(this.kai, true, 0.1, 0.1);
|
||||
this.cameras.main.setZoom(1.5);
|
||||
this.cursors = this.input.keyboard.createCursorKeys();
|
||||
|
||||
|
||||
|
||||
// Collider added later after Kai creation
|
||||
|
||||
// Collider added later after Kai creation
|
||||
|
||||
// --- 3. FOLIAGE (Trava - Šopi) ---
|
||||
// Removed as requested
|
||||
|
||||
// --- 4. ITEMS (Seno) ---
|
||||
// Removed as requested
|
||||
|
||||
|
||||
|
||||
// Collider Stream <-> Kai
|
||||
// this.physics.add.collider(this.kai, this.stream);
|
||||
|
||||
// --- ANIMATIONS ---
|
||||
// 0-3: Down, 4-7: Left, 8-11: Right, 12-15: Up
|
||||
// This is a duplicate animation creation block, removing it.
|
||||
// this.anims.create({
|
||||
// key: 'walk-down',
|
||||
// frames: this.anims.generateFrameNumbers('kai', { start: 0, end: 3 }),
|
||||
// frameRate: 8,
|
||||
// repeat: -1
|
||||
// });
|
||||
// this.anims.create({
|
||||
// key: 'walk-left',
|
||||
// frames: this.anims.generateFrameNumbers('kai', { start: 4, end: 7 }),
|
||||
// frameRate: 8,
|
||||
// repeat: -1
|
||||
// });
|
||||
// this.anims.create({
|
||||
// key: 'walk-right',
|
||||
// frames: this.anims.generateFrameNumbers('kai', { start: 8, end: 11 }),
|
||||
// frameRate: 8,
|
||||
// repeat: -1
|
||||
// });
|
||||
// this.anims.create({
|
||||
// key: 'walk-up',
|
||||
// frames: this.anims.generateFrameNumbers('kai', { start: 12, end: 15 }),
|
||||
// frameRate: 8,
|
||||
// repeat: -1
|
||||
// });
|
||||
|
||||
// Launch UI Scene
|
||||
if (!this.scene.get('UIScene').scene.settings.active) {
|
||||
this.scene.launch('UIScene');
|
||||
@@ -210,10 +230,5 @@ export default class GrassSceneClean extends Phaser.Scene {
|
||||
// --- Z-SORTING SYSTEM ---
|
||||
// Player
|
||||
this.kai.setDepth(this.kai.y);
|
||||
|
||||
// Camp Sorting
|
||||
this.campGroup.children.each(item => {
|
||||
item.setDepth(item.y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||