Skip to content

Commit

Permalink
major restructure
Browse files Browse the repository at this point in the history
  • Loading branch information
Dnyarri committed Apr 4, 2024
1 parent 5b68668 commit 9c4d38e
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 42 deletions.
12 changes: 2 additions & 10 deletions README.RU.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,9 @@

![Example of img2mesh output rendering](https://dnyarri.github.io/imgmesh/640/img2mesh.png)

*Версии*
В данной директории находится наиболее свежая версия программы; несколько более старых закопаны в *"old_versions"* для археологов будущего.

- **img2mesh.01.xxx** - конвертирует квадрат 2x2 пикселя в 2 треугольника с общей гипотенузой. Версия 01.003 - выбор диагонали квадрата для гипотенуз ("перегиба" квадрата) в соответствии с локальным градиентом. **Разработка прекращена на версии 01.004 в пользу версии 02**, обеспечивающей улучшенный рендеринг.

- **img2mesh.02.xxx** - конвертирует 1 пиксель в пирамиду из 4 треугольников. Узлы сетки автоматически связываются, что обеспечивает возможность использования сетки в [CSG](https://www.povray.org/documentation/3.7.0/r3_4.html#r3_4_5_4)-операциях (начиная с версии 02.002 к сцене добавлена операция intersection с кубом, что придаёт сетке боковые поверхности и дно. Редактируя размеры куба (или заменяя его на цилиндр и т.п.), можно аккуратно обрезать бока сетки, а также менять "уровень воды", прорезая отверстия на месте минимумов).

- **img2mesh.02.005** - существенное внутреннее изменение. Модуль чтения исходных PNG изменен с Pillow на [PyPNG](https://gitlab.com/drj11/pypng), что уменьшает объем внешних зависимостей программы, а главное - обеспечивает трассировку исходных PNG с цветовой глубиной 16 бит на канал, что резко увеличивает разрешение сигнала по оси z.

- **img2mesh.02.007** - внутренние изменения, улучшения читаемости вывода; к релизу 2.6.1 добавлен **img2mesh.exe** для Windows

*Зависимости от внешних библиотек:* Tkinter, PyPNG / Pillow
*Зависимости от внешних библиотек:* [PyPNG](https://gitlab.com/drj11/pypng), Tkinter. Первая лежит рядом с программой в репозитории, и, слава Создателю, способна работать в таком виде без установки; вторая входит во все типовые дистрибутивы Python.

*Инструкция по эксплуатации:* программы оборудованы минималистическим GUI, в результате всё, что вы должны сделать после запуска программы, это с помощью окна "Open..." выбрать и открыть файл PNG, с помощью окна "Save..." выбрать POV-файл для сохранения, подождать, пока программа отработает и закроется, затем открыть полученный POV-файл в POVRay и нажать кнопочку "Render". Экспортированная сцена содержит необходимый минимум глобальных переменных и объектов (камера, свет) для ознакомительного рендеринга без редактирования. Текстуры объекта и параметры камеры и т.п. записаны в максимально общем виде с комментариями, и должны быть просты для понимания и редактирования.

Expand Down
18 changes: 5 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
**(EN)** [(RU)](README.RU.md)

# Bitmap to 3D triangle mesh converter
# Bitmap to POVRay 3D triangle mesh converter

Python utilities for conversion of bitmap heightfield (in [PNG format](http://www.libpng.org/pub/png/)) to 3D triangle mesh in [POVRay](https://www.povray.org/) format. Resulting triangle mesh provides better rendering in case of low-res source files as compared to using source bitmaps as a heightfield directly.
Python program for conversion of bitmap heightfield (in [PNG format](http://www.libpng.org/pub/png/)) to 3D triangle mesh in [POVRay](https://www.povray.org/) format. Resulting triangle mesh provides better rendering in case of low-res source files as compared to using source bitmaps as a heightfield directly.

![Example of img2mesh output rendering](https://dnyarri.github.io/imgmesh/640/img2mesh.png)

*Version history*
Current dir contain most recent version of img2mesh program. Some previous versions are saved in *"old_versions"* for future alien archeologist to dig.

- **img2mesh.01.xxx** - converts 2x2 pixel square into 2 triangle. ver.01.003 - folding according to local gradient. **Development cancelled at ver.01.004 in favour of ver.02**, which provides better rendering.
*Dependencies:* [PyPNG](https://gitlab.com/drj11/pypng), Tkinter. The former is placed in this repo and, thank the Maker, will work right after downloading; the latter included in all typical Python installation.

- **img2mesh.02.xxx** - converts 1 pixel into pyramid of 4 triangles, significantly improving visual appearance of rendering. Mesh is tight enough to be used in [CSG](https://www.povray.org/documentation/3.7.0/r3_4.html#r3_4_5_4) (since version 02.002 added intersection with bounding box, thus giving sides and bottom to mesh).

- **img2mesh.02.005** - major internal change. Input module changed from Pillow to [PyPNG](https://gitlab.com/drj11/pypng) thus allowing 16 bpc PNG files to be processed, generating meshes with higher z-resolution.

- **img2mesh.02.007** - moderate internal changes; **Windows img2mesh.exe** file generated and added to 2.6.1 release.

*Dependencies:* Tkinter, PyPNG / Pillow

*Usage:* programs are equipped with minimal GUI, so all you have to do after starting the programs is use standard "Open..." GUI to open image file, then use standard "Save..." GUI to set POVRay scene file to be created, then wait while program does the job, then open resulting POV file with POVRay and render the scene. Scene contains enough basic stuff (globals, light, camera) to be rendered successfully right after exporting without any editing.
*Usage:* program equipped with minimal GUI, so all you have to do after starting the program is use standard "Open..." GUI to open image file, then use standard "Save..." GUI to set POVRay scene file to be created, then wait while program does the job, then open resulting POV file with POVRay and render the scene. Scene contains enough basic stuff (globals, light, camera) to be rendered successfully right after exporting without any editing.

More software at:

Expand Down
252 changes: 252 additions & 0 deletions img2mesh.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
#!/usr/bin/env python

'''
IMG2MESH - Program for conversion of image heightfield to triangle mesh in POVRay format
-----------------------------------------------------------------------------------------
Created by: Ilya Razmanov (mailto:[email protected])
aka Ilyich the Toad (mailto:[email protected])
History:
001 Abandoned img2mesh and turned to img2mesh2 with completely different mesh structure.
002 Added mesh encapsulation with cubic box to provide solid walls and bottom.
003 Restructured output for easy reading, everything but globals and includes now at the end of scene.
Extended camera description.
004 Bug with positioning found and seem to be fixed.
005 Replaced Pillow I/O with PyPNG from: https://gitlab.com/drj11/pypng
Support for 16 bit/channel PNGs added.
006 Minor output cleanup and generalization.
007 GUI improved to show progress during long processing. Attempt to reduce import.
2.7.1.0 Significant code cleanup with .writelines. Versioning more clear.
Main site:
https://dnyarri.github.io
Project mirrored at:
https://github.com/Dnyarri/img2mesh
https://gitflic.ru/project/dnyarri/img2mesh
'''

__author__ = "Ilya Razmanov"
__copyright__ = "(c) 2023-2024 Ilya Razmanov"
__credits__ = "Ilya Razmanov"
__license__ = "unlicense"
__version__ = "2.7.1.0"
__maintainer__ = "Ilya Razmanov"
__email__ = "[email protected]"
__status__ = "Production"

from tkinter import Tk
from tkinter import Label
from tkinter import filedialog
from time import time
from time import ctime

from png import Reader # I/O with PyPNG from: https://gitlab.com/drj11/pypng

# --------------------------------------------------------------
# Creating dialog

sortir = Tk()
sortir.title('PNG to POV conversion')
sortir.geometry('+100+100')
zanyato = Label(sortir, text = 'Starting...', font=("arial", 14), padx=16, pady=10, justify='center')
zanyato.pack()
sortir.withdraw()

# Main dialog created and hidden
# --------------------------------------------------------------

# Open source image
sourcefilename = filedialog.askopenfilename(title='Open source PNG file', filetypes=[('PNG','.png')], defaultextension = ('PNG','.png'))
if (sourcefilename == ''):
quit()

source = Reader(filename = sourcefilename) # starting PyPNG

X,Y,pixels,info = source.asDirect() # Opening image, iDAT comes to "pixels" as bytearray, to be tuple'd later

Z = (info['planes']) # Maximum CHANNEL NUMBER
imagedata = tuple((pixels)) # Attempt to fix all bytearrays

if (info['bitdepth'] == 8):
maxcolors = 255 # Maximal value for 8-bit channel
if (info['bitdepth'] == 16):
maxcolors = 65535 # Maximal value for 16-bit channel

# Open export file
resultfile = filedialog.asksaveasfile(mode='w', title='Save resulting POV file', filetypes =
[
('POV-Ray scene file', '*.pov'),
('All Files', '*.*'),
],
defaultextension = ('POV-Ray scene file','.pov'))
if (resultfile == ''):
quit()
# Both files opened

# src a-la FM style src(x,y,z)
# Image should be opened as "imagedata" by main program before
# Note that X, Y, Z are not determined in function, you have to determine it in main program

def src(x, y, z):
'''
Analog src from FM, force repeate edge instead of out of range
'''
cx = x; cy = y
cx = max(0,cx); cx = min((X-1),cx)
cy = max(0,cy); cy = min((Y-1),cy)

position = (cx*Z) + z # Here is the main magic of turning two x, z into one array position
channelvalue = int(((imagedata[cy])[position]))

return channelvalue
# end of src function

def srcY(x, y):
'''
Converting to greyscale, returns Yntensity, force repeate edge instead of out of range
'''
cx = x; cy = y
cx = max(0,cx); cx = min((X-1),cx)
cy = max(0,cy); cy = min((Y-1),cy)

if (info['planes'] < 3): # supposedly L and LA
Yntensity = src(x, y, 0)
else: # supposedly RGB and RGBA
Yntensity = int(0.2989*src(x, y, 0) + 0.587*src(x, y, 1) + 0.114*src(x, y, 2))

return Yntensity
# end of srcY function

# WRITING POV FILE

# ------------
# POV header
# ------------

resultfile.writelines(['/*\n',
'Persistence of Vision Ray Tracer Scene Description File\n',
'Version: 3.7\n',
'Description: A triangle mesh file converted from image heightfield\n',
'Author: Automatically generated by img2mesh Pyton program',
'https://github.com/Dnyarri/img2mesh\n',
'https://gitflic.ru/project/dnyarri/img2mesh\n',
'developed by Ilya Razmanov aka Ilyich the Toad\n',
'https://dnyarri.github.io\nmailto:[email protected]\n',
'*/\n\n'
])

resultfile.write(f'// Converted from: {sourcefilename} ')
seconds = time(); localtime = ctime(seconds)
resultfile.write(f'at: {localtime}\n// Source info: {info}\n\n')

# Statements

resultfile.writelines(['\n',
'#version 3.7;\n\n',
'global_settings{\n',
' max_trace_level 3 // Small to speed up preview. May need to be increased for metals\n',
' adc_bailout 0.01 // High to speed up preview. May need to be decreased to 1/256\n',
' ambient_light <0.5,0.5,0.5>\n',
' assumed_gamma 1.0\n}\n',
'\n#include "colors.inc"\n#include "finish.inc"\n#include "golds.inc"\n#include "metals.inc"'
'\n\n'
])

# Mesh

resultfile.write('#declare thething = mesh {\n') # Opening mesh object "thething"

# Now going to cycle through image and build mesh

for y in range(0, Y, 1):

message = ('Processing row ' + str(y) +' of ' + str(Y) + '...')
sortir.deiconify()
zanyato.config(text = message)
sortir.update()
sortir.update_idletasks()

resultfile.write(f'\n\n // Row {y}\n')

for x in range(0, X, 1):

v9 = srcY(x,y) # Current pixel to process and write. Then going to neighbours
v1 = (v9 + srcY((x-1), y) + srcY((x-1), (y-1)) + srcY(x, (y-1)))/4.0 # По улитке 8-1-2
v3 = (v9 + srcY(x, (y-1)) + srcY((x+1), (y-1)) + srcY((x+1), y))/4.0 # По улитке 2-3-4
v5 = (v9 + srcY((x+1), y) + srcY((x+1), (y+1)) + srcY(x, (y+1)))/4.0 # По улитке 4-5-6
v7 = (v9 + srcY(x, (y+1)) + srcY((x-1), (y+1)) + srcY((x-1), y))/4.0 # По улитке 6-7-8

# going to pyramid building

resultfile.write('\n triangle{') # Opening triangle 2
resultfile.write(f'<{(x-0.5)}, {(y-0.5)}, {v1}> ')
resultfile.write(f'<{(x+0.5)}, {(y-0.5)}, {v3}> ')
resultfile.write(f'<{x}, {y}, {v9}>')
resultfile.write('}') # Closing triangle 2

resultfile.write('\n triangle{') # Opening triangle 4
resultfile.write(f'<{(x+0.5)}, {(y-0.5)}, {v3}> ')
resultfile.write(f'<{(x+0.5)}, {(y+0.5)}, {v5}> ')
resultfile.write(f'<{x}, {y}, {v9}>')
resultfile.write('}') # Closing triangle 4

resultfile.write('\n triangle{') # Opening triangle 6
resultfile.write(f'<{(x+0.5)}, {(y+0.5)}, {v5}> ')
resultfile.write(f'<{(x-0.5)}, {(y+0.5)}, {v7}> ')
resultfile.write(f'<{x}, {y}, {v9}>')
resultfile.write('}') # Closing triangle 6

resultfile.write('\n triangle{') # Opening triangle 8
resultfile.write(f'<{(x-0.5)}, {(y+0.5)}, {v7}> ')
resultfile.write(f'<{(x-0.5)}, {(y-0.5)}, {v1}> ')
resultfile.write(f'<{x}, {y}, {v9}>')
resultfile.write('}') # Closing triangle 8

resultfile.write('\n\ninside_vector <0, 0, 1>\n\n')

# Transform object to fit 1, 1, 1 cube at 0, 0, 0 coordinates
resultfile.write('\n// Object transforms to fit 1, 1, 1 cube at 0, 0, 0 coordinates\n')
resultfile.write('translate <0.5, 0.5, 0>\n') # compensate for -0.5 extra, now object fit 0..X, 0..Y, 0..maxcolors
resultfile.write(f'translate <-0.5*{X}, -0.5*{Y}, 0>\n') # translate to center object bottom at x = 0, y = 0, z = 0
resultfile.write(f'scale <-1.0/{max(X,Y)}, -1.0/{max(X,Y)}, 1.0/{maxcolors}>\n') # rescale, mirroring POV coordinates to match Photoshop coordinate system

# Sample texture of textures
resultfile.writelines(['texture {\n',
' gradient z\n',
' texture_map {\n',
' [0.01 pigment {Red} finish {phong 1}]\n',
' [0.5 pigment {Blue} finish {phong 5}]\n',
' [0.99 pigment {White} finish {phong 10}]\n }\n}\n',
'}\n// Closed thething\n\n',
'#declare boxedthing = object{\n',
' intersection {\n',
' box {<-0.5, -0.5, 0>, <0.5, 0.5, 1.0>\n',
' pigment {rgb <0.5, 0.5, 5>}\n',
' }\n thething\n }\n}',
'// Constructed CGS "boxedthing" of mesh plus bounding box thus adding side walls and bottom\n\n',
'object {boxedthing}\n\n'
]) # Closing mesh object "thething", then bounding box

# Camera
proportions = max(X,Y)/X
resultfile.write('#declare camera_height = 3.0;\n\n')
resultfile.write('camera {\n // orthographic\n location <0.0, 0.0, camera_height>\n right x*image_width/image_height\n up y\n direction <0, 0, 1>\n angle 2.0*(degrees(atan2(')
resultfile.write(f'{0.5 * proportions}')
resultfile.write(', camera_height-1.0))) // Supposed to fit object \n look_at <0.0, 0.0, 0.0>\n}\n\n')

# Light
resultfile.write('light_source {0*x\n color rgb <1,1,1>\n translate <20, 20, 20>\n}\n')
resultfile.write('\n/*\n\nhappy rendering\n\n 0~0\n (---)\n(.>|<.)\n-------\n\n*/')
# Close output
resultfile.close()

# --------------------------------------------------------------
# Destroying dialog

sortir.destroy()
sortir.mainloop()

# Dialog destroyed and closed
# --------------------------------------------------------------
Loading

0 comments on commit 9c4d38e

Please sign in to comment.