Блинн-Фонг затенение с Numpy - PullRequest
0 голосов
/ 03 марта 2019

Я пытаюсь реализовать затенение blinn-phong в numpy для образовательных целей.Однако я застрял в отладке того, что параметры делают в течение нескольких дней.

Моя общая идея заключалась в следующем.Так как уравнение было дано для канала.Я применяю модель к каждому цветному каналу, чтобы получить относительную интенсивность пикселей в канале, а затем перегруппирую каналы, чтобы получить все изображение.

Мой коэффициент Ламберта, кажется, не учитывает изменения положения света, но это меняет интенсивность пикселей, но другие параметры почти не влияют на вывод.Любая помощь будет оценена.Вот относительные биты кода (полный код здесь для всех, кто заинтересован):


def normalize_1d_array(arr):
    "Normalize 1d array"
    assert arr.ndim == 1
    result = None
    if np.linalg.norm(arr) == 0:
        result = arr
    else:
        result = arr / np.linalg.norm(arr)
    return result


def normalize_3col_array(arr):
    "Normalize 3 column array"
    assert arr.shape[1] == 3
    assert arr.ndim == 2
    normal = np.copy(arr)
    normal[:, 0] = normalize_1d_array(normal[:, 0])
    normal[:, 1] = normalize_1d_array(normal[:, 1])
    normal[:, 2] = normalize_1d_array(normal[:, 2])
    return normal


def get_vector_dot(arr1, arr2):
    "Get vector dot product for 2 matrices"
    assert arr1.shape == arr2.shape
    newarr = np.sum(arr1 * arr2, axis=1, dtype=np.float32)
    return newarr


class LightSource:
    "Simple implementation of a light source"

    def __init__(self,
                 x=10.0,  # x
                 y=5.0,  # y
                 z=0.0,  # light source at infinity
                 intensity=1.0,  # I_p
                 ambient_intensity=1.0,  # I_a
                 ambient_coefficient=0.1,  # k_a
                 light_power=80.0):
        "light source"
        self.x = x
        self.y = y
        if z is not None:
            assert isinstance(z, float)
        self.z = z
        self.intensity = intensity
        self.power = light_power
        self.ambient_intensity = ambient_intensity  # I_a
        self.ambient_coefficient = ambient_coefficient  # k_a
        # k_a can be tuned if the material is known

    def copy(self):
        "copy self"
        return LightSource(x=self.x,
                           y=self.y,
                           z=self.z,
                           intensity=self.intensity,
                           light_power=self.power)


class ChannelShader:
    "Shades channels"

    def __init__(self,
                 coordarr: np.ndarray,
                 light_source: LightSource,  # has I_a, I_p, k_a
                 surface_normal: np.ndarray,
                 imagesize: (int, int),
                 color: np.ndarray,  # they are assumed to be O_d and O_s
                 spec_coeff=0.5,  # k_s
                 screen_gamma=2.2,
                 diffuse_coeff=0.9,  # k_d
                 attenuation_c1=2.0,  # f_attr c1
                 attenuation_c2=0.0,  # f_attr c2 d_L coefficient
                 attenuation_c3=0.0,  # f_attr c3 d_L^2 coefficient
                 shininess=270.0  # n
                 ):
        self.light_source = light_source
        self.light_intensity = self.light_source.intensity  # I_p
        self.ambient_coefficient = self.light_source.ambient_coefficient  # k_a
        self.ambient_intensity = self.light_source.ambient_intensity  # I_a
        self.coordarr = coordarr
        self.surface_normal = np.copy(surface_normal)
        self.screen_gamma = screen_gamma
        self.shininess = shininess
        self.diffuse_coeff = diffuse_coeff  # k_d
        self.diffuse_color = normalize_1d_array(color)  # O_d: object diffuse color
        self.spec_color = normalize_1d_array(color)  # O_s: object specular color
        self.spec_coeff = spec_coeff  # k_s: specular coefficient
        self.imsize = imagesize
        self.att_c1 = attenuation_c1
        self.att_c2 = attenuation_c2
        self.att_c3 = attenuation_c3

    def copy(self):
        return ChannelShader(coordarr=np.copy(self.coordarr),
                             light_source=self.light_source.copy(),
                             surface_normal=np.copy(self.surface_normal),
                             color=np.copy(self.diffuse_coeff) * 255.0)

    @property
    def distance(self):
        yarr = self.coordarr[:, 0]  # row nb
        xarr = self.coordarr[:, 1]  # col nb
        xdist = (self.light_source.x - xarr)**2
        ydist = (self.light_source.y - yarr)**2
        return xdist + ydist

    @property
    def distance_factor(self):
        resx = self.imsize[1]
        factor = self.distance / self.light_source.z * resx
        return 1.0 - factor

    @property
    def light_direction(self):
        "get light direction matrix (-1, 3)"
        yarr = self.coordarr[:, 0]
        xarr = self.coordarr[:, 1]
        xdiff = self.light_source.x - xarr
        ydiff = self.light_source.y - yarr
        light_matrix = np.zeros((self.coordarr.shape[0], 3))
        light_matrix[:, 0] = ydiff
        light_matrix[:, 1] = xdiff
        light_matrix[:, 2] = self.light_source.z
        # light_matrix[:, 2] = 0.0
        return light_matrix

    @property
    def light_attenuation(self):
        """
        Implementing from Foley JD 1996, p. 726

        f_att : light source attenuation function:
        f_att = min(\frac{1}{c_1 + c_2{\times}d_L + c_3{\times}d^2_{L}} , 1)
        """
        second = self.att_c2 * self.distance
        third = self.att_c3 * self.distance * self.distance
        result = self.att_c1 + second + third
        result = 1 / result
        return np.where(result < 1, result, 1)

    @property
    def normalized_light_direction(self):
        "Light Direction matrix normalized"
        return normalize_3col_array(self.light_direction)

    @property
    def normalized_surface_normal(self):
        return normalize_3col_array(self.surface_normal)

    @property
    def costheta(self):
        "set costheta"
        # pdb.set_trace()
        costheta = get_vector_dot(
            arr1=self.normalized_light_direction,
            arr2=self.normalized_surface_normal)
        # products of vectors
        costheta = np.abs(costheta)  # as per (Foley J.D, et.al. 1996, p. 724)
        return costheta

    @property
    def ambient_term(self):
        "Get the ambient term I_a * k_a * O_d"
        term = self.ambient_coefficient * self.ambient_intensity
        return term * self.diffuse_color

    @property
    def view_direction(self):
        "Get view direction"
        # pdb.set_trace()
        cshape = self.coordarr.shape
        coord = np.zeros((cshape[0], 3))  # x, y, z
        coord[:, :2] = -self.coordarr
        coord[:, 2] = 0.0  # viewer at infinity
        coord = normalize_3col_array(coord)
        return coord

    @property
    def half_direction(self):
        "get half direction"
        # pdb.set_trace()
        arr = self.view_direction + self.normalized_light_direction
        return normalize_3col_array(arr)

    @property
    def spec_angle(self):
        "get spec angle"
        specAngle = get_vector_dot(
            arr1=self.half_direction,
            arr2=self.normalized_surface_normal)
        return np.where(specAngle > 0.0, specAngle, 0.0)

    @property
    def specular(self):
        return self.spec_angle ** self.shininess

    @property
    def channel_color_blinn_phong(self):
        """compute new channel color intensities
        Implements: Foley J.D. 1996 p. 730 - 731, variation on equation 16.15
        """
        second = 1.0  # added for structuring code in this fashion, makes
        # debugging easier
        # lambertian terms
        second *= self.diffuse_coeff  # k_d
        second *= self.costheta  # (N \cdot L)
        second *= self.light_intensity  # I_p
        # adding phong terms
        second *= self.light_attenuation  # f_attr
        second *= self.diffuse_color  # O_d
        third = 1.0
        third *= self.spec_color  # O_s
        third *= self.specular  # (N \cdot H)^n
        third *= self.spec_coeff  # k_s
        result = 0.0
        result += self.ambient_term  # I_a × k_a × O_d
        result += second
        result += third
        pdb.set_trace()
        return result

Спасибо

1 Ответ

0 голосов
/ 05 марта 2019

В конце концов, у реализации не было много проблем, однако изображения, с которыми я работал, требовали очень странных значений для параметров из-за их специфического состояния, в котором они создаются.

Большинство использованных мною изображений содержали шероховатые поверхности с неглазурованной глиной в качестве материала, и изображения были получены в контролируемой среде с одним источником света, в отличие от реальных условий, где объекты освещаются из нескольких световых пятен.

Таким образом, большинство параметров, касающихся окружающего освещения и зеркального отражения, не имело особого смысла в использовании.

Я помещу сюда относительные части моей реализации в качестве справки для будущих пользователей, убедитесь, что НЕ использует значения по умолчанию .

Некоторые подробности о реализации:

  • Это в значительной степени соответствует уравнению, указанному в работе Foley JD et.al., 1996, p.730 - 731, № 16.15 с вариацией добавления вектора на полпути, необходимого для blinn-phong.

  • ChannelShader ожидает следующее:

    • Координаты пикселей канала формы: (-1, 2)
    • Поверхностные нормали формы: (-1, 3)
    • Цвета канала формы: (-1,)
    • Источник света типа LightSource
    • Как указано выше, пожалуйста, измените значения по умолчанию, прежде чем продолжить сэкспериментирование.

Вы можете использовать одинаковые нормали поверхности для каждого канала, если вы закрашиваете несколько каналов.

Последнее слово предостережения, оно заметно медленное, даже с пустышкой.Правильный способ рендеринга - это библиотеки на основе gpu, такие как pyopengl и т. Д. Я не тестировал его с портами gpu numpy, такими как cupy, и с другими библиотеками, такими как numba и т. Д., Хотя:


def normalize_1d_array(arr):
    "Normalize 1d array"
    assert arr.ndim == 1
    result = None
    if np.linalg.norm(arr) == 0:
        result = arr
    else:
        result = arr / np.linalg.norm(arr)
    return result

def normalize_3col_array(arr):
    "Normalize 3 column array"
    assert arr.shape[1] == 3
    assert arr.ndim == 2
    normal = np.copy(arr)
    normal[:, 0] = normalize_1d_array(normal[:, 0])
    normal[:, 1] = normalize_1d_array(normal[:, 1])
    normal[:, 2] = normalize_1d_array(normal[:, 2])
    return normal


def get_vector_dot(arr1, arr2):
    "Get vector dot product for 2 matrices"
    assert arr1.shape == arr2.shape
    newarr = np.sum(arr1 * arr2, axis=1, dtype=np.float32)
    return newarr


class ImageArray:
    "Image array have some additional properties besides np.ndarray"

    def __init__(self, image: np.ndarray):
        assert isinstance(image, np.ndarray)
        self.image = image

    @property
    def norm_coordinates(self):
        "Get normalized coordinates of the image pixels"
        # pdb.set_trace()
        rownb, colnb = self.image.shape[0], self.image.shape[1]
        norm = np.empty_like(self.coordinates, dtype=np.float32)
        norm[:, 0] = self.coordinates[:, 0] / rownb
        norm[:, 1] = self.coordinates[:, 1] / colnb
        return norm

    @property
    def norm_image(self):
        "Get normalized image with pixel values divided by 255"
        return self.image / 255.0

    @property
    def coordinates(self):
        "Coordinates of the image pixels"
        rownb, colnb = self.image.shape[:2]
        coords = [[(row, col) for col in range(colnb)] for row in range(rownb)]
        coordarray = np.array(coords)
        return coordarray.reshape((-1, 2))

    @property
    def arrshape(self):
        "get array shape"
        return self.image.shape

    @property
    def flatarr(self):
        "get flattened array"
        return self.image.flatten()


def interpolateImage(imarr: ImageArray):
    "Interpolate image array"
    imshape = imarr.image.shape
    newimage = imarr.image.flatten()
    newimage = np.uint8(np.interp(newimage,
                                  (newimage.min(),
                                   newimage.max()),
                                  (0, 255))
                        )
    newimage = newimage.reshape(imshape)
    return ImageArray(newimage)


class LightSource:
    "Simple implementation of a light source"

    def __init__(self,
                 x=1.0,  # x
                 y=1.0,  # y
                 z=20.0,  # light source distance: 0 to make it at infinity
                 intensity=1.0,  # I_p
                 ambient_intensity=1.0,  # I_a
                 ambient_coefficient=0.000000002,  # k_a
                 ):
        "light source"
        self.x = x
        self.y = y
        if z is not None:
            assert isinstance(z, float)
        self.z = z
        self.intensity = intensity
        self.ambient_intensity = ambient_intensity  # I_a
        self.ambient_coefficient = ambient_coefficient  # k_a
        # k_a can be tuned if the material is known

    def copy(self):
        "copy self"
        return LightSource(x=self.x,
                           y=self.y,
                           z=self.z,
                           intensity=self.intensity,
                           light_power=self.power)


class ChannelShader:
    "Shades channels"

    def __init__(self,
                 coordarr: np.ndarray,
                 light_source: LightSource,  # has I_a, I_p, k_a
                 surface_normal: np.ndarray,
                 color: np.ndarray,  # they are assumed to be O_d and O_s
                 spec_coeff=0.1,  # k_s
                 spec_color=1.0,  # O_s: obj specular color. It can be
                 # optimized with respect to surface material
                 screen_gamma=2.2,
                 diffuse_coeff=0.008,  # k_d
                 # a good value is between 0.007 and 0.1
                 attenuation_c1=1.0,  # f_attr c1
                 attenuation_c2=0.0,  # f_attr c2 d_L coefficient
                 attenuation_c3=0.0,  # f_attr c3 d_L^2 coefficient
                 shininess=20.0  # n
                 ):
        self.light_source = light_source
        self.light_intensity = self.light_source.intensity  # I_p
        self.ambient_coefficient = self.light_source.ambient_coefficient  # k_a
        self.ambient_intensity = self.light_source.ambient_intensity  # I_a
        self.coordarr = coordarr
        self.surface_normal = np.copy(surface_normal)
        self.screen_gamma = screen_gamma
        self.shininess = shininess
        self.diffuse_coeff = diffuse_coeff  # k_d
        # self.diffuse_color = normalize_1d_array(color)  # O_d: obj diffuse color
        self.diffuse_color = color  # O_d: obj diffuse color
        self.spec_color = spec_color  # O_s
        self.spec_coeff = spec_coeff  # k_s: specular coefficient
        self.att_c1 = attenuation_c1
        self.att_c2 = attenuation_c2
        self.att_c3 = attenuation_c3

    def copy(self):
        return ChannelShader(coordarr=np.copy(self.coordarr),
                             light_source=self.light_source.copy(),
                             surface_normal=np.copy(self.surface_normal),
                             color=np.copy(self.diffuse_color))

    @property
    def distance(self):
        yarr = self.coordarr[:, 0]  # row nb
        xarr = self.coordarr[:, 1]  # col nb
        xdist = (self.light_source.x - xarr)**2
        ydist = (self.light_source.y - yarr)**2
        return xdist + ydist

    @property
    def light_direction(self):
        "get light direction matrix (-1, 3)"
        yarr = self.coordarr[:, 0]
        xarr = self.coordarr[:, 1]
        xdiff = self.light_source.x - xarr
        ydiff = self.light_source.y - yarr
        light_matrix = np.zeros((self.coordarr.shape[0], 3))
        light_matrix[:, 0] = ydiff
        light_matrix[:, 1] = xdiff
        light_matrix[:, 2] = self.light_source.z
        # light_matrix[:, 2] = 0.0
        # pdb.set_trace()
        return light_matrix

    @property
    def light_attenuation(self):
        """
        Implementing from Foley JD 1996, p. 726

        f_att : light source attenuation function:
        f_att = min(\frac{1}{c_1 + c_2{\times}d_L + c_3{\times}d^2_{L}} , 1)
        """
        second = self.att_c2 * self.distance
        third = self.att_c3 * self.distance * self.distance
        result = self.att_c1 + second + third
        result = 1 / result
        return np.where(result < 1, result, 1)

    @property
    def normalized_light_direction(self):
        "Light Direction matrix normalized"
        return normalize_3col_array(self.light_direction)

    @property
    def normalized_surface_normal(self):
        return normalize_3col_array(self.surface_normal)

    @property
    def costheta(self):
        "set costheta"
        # pdb.set_trace()
        costheta = get_vector_dot(
            arr1=self.normalized_light_direction,
            arr2=self.normalized_surface_normal)
        # products of vectors
        # costheta = np.abs(costheta)  # as per (Foley J.D, et.al. 1996, p. 724)
        costheta = np.where(costheta > 0, costheta, 0)
        return costheta

    @property
    def ambient_term(self):
        "Get the ambient term I_a * k_a * O_d"
        term = self.ambient_coefficient * self.ambient_intensity
        term *= self.diffuse_color
        # pdb.set_trace()
        return term

    @property
    def view_direction(self):
        "Get view direction"
        # pdb.set_trace()
        cshape = self.coordarr.shape
        coord = np.zeros((cshape[0], 3))  # x, y, z
        coord[:, :2] = -self.coordarr
        coord[:, 2] = 0.0  # viewer at infinity
        coord = normalize_3col_array(coord)
        return coord

    @property
    def half_direction(self):
        "get half direction"
        # pdb.set_trace()
        arr = self.view_direction + self.normalized_light_direction
        return normalize_3col_array(arr)

    @property
    def spec_angle(self):
        "get spec angle"
        specAngle = get_vector_dot(
            arr1=self.half_direction,
            arr2=self.normalized_surface_normal)
        return np.where(specAngle > 0.0, specAngle, 0.0)

    @property
    def specular(self):
        return self.spec_angle ** self.shininess

    @property
    def channel_color_blinn_phong(self):
        """compute new channel color intensities
        Implements: Foley J.D. 1996 p. 730 - 731, variation on equation 16.15
        """
        second = 1.0  # added for structuring code in this fashion, makes
        # debugging easier
        # lambertian terms
        second *= self.diffuse_coeff  # k_d
        second *= self.costheta  # (N \cdot L)
        second *= self.light_intensity  # I_p
        # adding phong terms
        second *= self.light_attenuation  # f_attr
        second *= self.diffuse_color  # O_d
        third = 1.0
        third *= self.spec_color  # O_s
        third *= self.specular  # (N \cdot H)^n
        third *= self.spec_coeff  # k_s
        result = 0.0
        #
        result += self.ambient_term  # I_a × k_a × O_d
        result += second
        result += third
        # pdb.set_trace()
        return result
...